详解Spring Cache使用Redisson分布式锁解决缓存击穿问题

目录
  • 1 什么是缓存击穿
  • 2 为什么要使用分布式锁
  • 3 什么是Redisson
  • 4 Spring Boot集成Redisson
    • 4.1 添加maven依赖
    • 4.2 配置yml
    • 4.3 配置RedissonConfig
  • 5 使用Redisson的分布式锁解决缓存击穿

1 什么是缓存击穿

一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。

2 为什么要使用分布式锁

在项目中,当共享资源出现竞争情况的时候,为了防止出现并发问题,我们一般会采用锁机制来控制。在单机环境下,可以使用synchronized或Lock来实现;但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如mysql、redis、zookeeper。

3 什么是Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

4 Spring Boot集成Redisson

4.1 添加maven依赖

不再需要spring-boot-starter-data-redis依赖,但是都添加也不会报错

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>

4.2 配置yml

spring:
  datasource:
    username: xx
    password: xxxxxx
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=CTT
  cache:
    type: redis
  redis:
    database: 0
    port: 6379               # Redis服务器连接端口
    host: localhost          # Redis服务器地址
    password: xxxxxx         # Redis服务器连接密码(默认为空)
    timeout: 5000            # 超时时间

4.3 配置RedissonConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Random;

@EnableCaching
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean(destroyMethod = "shutdown")  // bean销毁时关闭Redisson实例,但不关闭Redis服务
    public RedissonClient redisson() {
        //创建配置
        Config config = new Config();
        /**
         *  连接哨兵:config.useSentinelServers().setMasterName("myMaster").addSentinelAddress()
         *  连接集群: config.useClusterServers().addNodeAddress()
         */
        config.useSingleServer()
                .setAddress("redis://" + host + ":" + port)
                .setPassword(password)
                .setTimeout(5000);
        //根据config创建出RedissonClient实例
        return Redisson.create(config);
    }

    @Bean
    public CacheManager RedisCacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        /**
         * 新版本中om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL)已经被废弃
         * 建议替换为om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL)
         */
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化解决乱码的问题
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 设置缓存过期时间  为解决缓存雪崩,所以将过期时间加随机值
                .entryTtl(Duration.ofSeconds(60 * 60 + new Random().nextInt(60 * 10)))
                // 设置key的序列化方式
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                // 设置value的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        // .disableCachingNullValues(); //为防止缓存击穿,所以允许缓存null值
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                // 启用RedisCache以将缓存 put/evict 操作与正在进行的 Spring 管理的事务同步
                .transactionAware()
                .build();
        return cacheManager;
    }
}

5 使用Redisson的分布式锁解决缓存击穿

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.company.dubbodemo.entity.User;
import com.company.dubbodemo.mapper.UserMapper;
import com.company.dubbodemo.service.UserService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {
    
    @Resource
    private RedissonClient redissonClient;

    @Resource
    private UserMapper userMapper;

    @Override
    // 一定要设置sync = true开启异步,否则会导致多个线程同时获取到锁
    @Cacheable(cacheNames = "user", key = "#id", sync = true)
    public User findById(Long id) {
        /**
         *
         * 加了@Cacheable之后方法体执行说明缓存中不存在所查询的数据
         * 获取一把锁,只要锁的名字一样,就是同一把锁
         */

        /**
         * 注意: 如果设置了lock.lock(10,TimeUnit.SECONDS) 锁过期不会自动续期
         *      1、如果我们传递了锁的过期时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
         *      2、如果没有指定锁的超时时间,就使用30000L(LockWatchdogTimeout 看门狗的默认时间)
         *      可通过RedissonConfig-->getRedissonClient()-->config.setLockWatchdogTimeout()设置看门狗时间
         *         只要占锁成功就会启动一个定时任务【就会重新给锁设置过期时间,新的时间就是看门狗的默认时间】,每隔10s都会自动续期,续期成30s
         * 看门狗机制
         * 1、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心因为业务时间长,锁自动过期被删除
         * 2、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
         *
         */
        RLock lock = redissonClient.getLock("redissonClient-lock");

        // 对第一个线程执行方法体的线程加锁,加了@Cacheable,方法执行之后会将方法的返回值存入缓存,下一个线程直接读取缓存
        lock.lock();
        User user = new User();
        try {
            user = userMapper.selectById(id);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return user;
    }
}

到此这篇关于详解Spring Cache使用Redisson分布式锁解决缓存击穿问题的文章就介绍到这了,更多相关Spring Cache 缓存击穿内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2022-04-23

使用spring-cache一行代码解决缓存击穿问题

目录 引言 正文 目前缺陷 真正方案 缓存穿透 缓存击穿 缓存雪崩 文末 引言 今天,重新回顾一下缓存击穿这个问题! 之所以写这个文章呢,因为目前网上流传的文章落地性太差(什么布隆过滤器啊,布谷过滤器啊,嗯,你们懂的),其实这类方案并不适合在项目中直接落地. 那么,我们在项目中落地代码的时候,其实只需要一个注解就能解决这些问题,并不需要搞的那么复杂. 本文有一个前提,读者必须是java栈,且是用Springboot构建自己的项目,如果是go技术栈或者python技术栈的,可能介绍的思路仅供大家参

spring boot+spring cache实现两级缓存(redis+caffeine)

spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis.Caffeine.JCache.EhCache等等.但如果只用一种缓存,要么会有较大的网络消耗(如Redis),要么就是内存占用太大(如Caffeine这种应用内存缓存).在很多场景下,可以结合起来实现一.二级缓存的方式,能够很大程度提高应用的处理效率. 内容说明: 缓存.两级缓存 spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明 spring boot

Spring Cache手动清理Redis缓存

这篇文章主要介绍了Spring Cache手动清理Redis缓存,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 注册cacheRedisTemplate 将 cache 的 RedisTemplate 注册为Bean @Bean(name = "cacheRedisTemplate") public RedisTemplate cacheRedisTemplate(@Qualifier("jedisConnectionFac

Python一行代码解决矩阵旋转的问题

今天刷<剑指offer>的时候碰到这样一道题: 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10. 思路:可以模拟魔方逆时针旋转的方法,一直做取出第一行的操作,例如 : 1 2 3 4 5 6 7 8 9 利用pop输出并且删除第一行后,再进行一次逆时针旋转,就变成: 6 9

一行代码解决 perl输入 排序 输出问题

先来看基本的方法: 复制代码 代码如下: chomp(@strings=<STDIN>);  @strings=sort @strings;  print "@strings\n."; 上面的三行代码解决的是输入,然后排序,最后将排序后的结果输出的作用. 只用一行代码即完成的写法: 复制代码 代码如下: print sort<STDIN>; 得到相同的结果,相当给力哦.

一行代码解决网站防挂IFRAME木马方案,小鸽子序列(灵儿)

最近公司和好朋友的网站纷纷被IFRAME了,有的挂上了鸽子,有的疯狂地弹窗,有的给人家增加流量.一个个文件去查找替换那些IFRAME代码,刚松口气,不久又加上去了,哎,什么世道!于是他们向我这个"JS高手"(他们强加给我的)求救,我也不能等闲视之,只好击键杀杀杀了. 因为FF(Firefox)不怕IFRAME,于是就拿IE开刀,不知道比尔有没有奖发.我只写了一句代码,就搞定了,哈,痛快.就是IE only(特有的)的CSS中的属性expression,插进去试试,果然那些IFRAME不

用expression的一行代码解决iframe挂马的问题

(包含服务器端注入.客户端ARP注入等)IE Only--一般只有IE害怕iframe这样的挂马,所以灵儿就拿IE开刀. 在阅读本文之前,我们先了解一下expression: IE5及其以后版本支持在CSS中使用expression,用来把CSS属性和JavaScript脚本关联起来,这里的CSS属性可以是元素固有的属性,也可以是自定义属性.就是说CSS属性后面可以是一段JavaScript表达式,CSS属性的值等于Javascript表达式执行的结果.在表达式中可以直接引用元素自身的属性和方法

springboot集成spring cache缓存示例代码

本文介绍如何在springboot中使用默认的spring cache, 声明式缓存 Spring 定义 CacheManager 和 Cache 接口用来统一不同的缓存技术.例如 JCache. EhCache. Hazelcast. Guava. Redis 等.在使用 Spring 集成 Cache 的时候,我们需要注册实现的 CacheManager 的 Bean. Spring Boot 为我们自动配置了 JcacheCacheConfiguration. EhCacheCacheCo

使用Golang的singleflight防止缓存击穿的方法

在使用缓存时,容易发生缓存击穿. 缓存击穿:一个存在的key,在缓存过期的瞬间,同时有大量的请求过来,造成所有请求都去读dB,这些请求都会击穿到DB,造成瞬时DB请求量大.压力骤增. singleflight 介绍 import "golang.org/x/sync/singleflight" singleflight类的使用方法就新建一个singleflight.Group,使用其方法Do或者DoChan来包装方法,被包装的方法在对于同一个key,只会有一个协程执行,其他协程等待那个

二行代码解决全部网页木马

前不久一行代码解决iframe挂马(包含服务器端注入.客户端ARP注入等)>得到了很多朋友的认可,这确实是个避避风雨的好办法.可现在挂网马的方式真如我所料地改变了,现在流行挂<script>木马,汗了,看了几个网友的网站都被这样了--页面的顶部或底部加上了: 注意,以下地址含有木马,请不要轻易访问: <script src=http://%76%63%63%64%2E%63%6E></script> <script src=http://%76%63%63%

Spring Cache整合Redis实现方法详解

导入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>