缓存与分布式锁

一、缓存

1.1 那些数据适合缓存

1)即时性,数据一致性要求不要的数据
2)访问量大且更新频率不高的数据(读多,写少)
举例::电商类应用,商品分类商品列表等适合缓存并加一个失效时间(根据数据更新频率
来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没
有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致
问题。

1.2 缓存的流程

在这里插入图片描述

1.2 本地缓存实例

    /**
     * 本地缓存
     */
    private Map<String,Object> mapCache = new HashMap<>();
    @ResponseBody
    @GetMapping(value = "/cashe")
    public String testCashe() {
    
    
        Object getCatalogJson  = mapCache.get("cat");
        if(getCatalogJson==null){
    
    
            // 从数据库查询
            getCatalogJson = this.getCatalogJson();
        }
        return getCatalogJson.toString();
    }

1.3 本地缓存在微服务中存在的问题

由于我们的微服务部署时可能不是部署一处,而是部署了多份,通过负载均衡去匹配请求,每一个服务中的本地缓存时不一样的,这样在调用的时候就会有问题。所以在微服务中我们应该使用分布式缓存,而不能使用本地缓存。

1.4 解决本地缓存在微服务中负载均衡不一致问题

把多个微服务的缓存保存在同一个位置,如redis

二、★ 整合 redis 作为缓存

第一步:添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

第二步:添加配置
6379 redis的默认端口,可以不写

spring:
  redis:
    host: 192.168.56.10
    port: 6379

第三步:使用 RedisTemplate 操作 redis

@Autowired
	StringRedisTemplate stringRedisTemplate;
	@Test
	public void testRedis(){
    
    
		ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
		// 保存
		ops.set("hello","world");
		// 查询
		String hello = ops.get("hello");
		System.out.println("maruis----保存的是数据时-->" + hello);
	}

第三步:StringRedisTemplate的更多用法
https://blog.csdn.net/weixin_43835717/article/details/92802040/
五大常用数据类型使用场景
String
缓存:将数据以字符串方式存储
计数器功能:比如视频播放次数,点赞次数。
共享session:数据共享的功能,redis作为单独的应用软件用来存储一些共享数据供多个实例访问。
字符串的使用空间非常大,可以结合字符串提供的命令充分发挥自己的想象力

hash
字典。键值对集合,即编程语言中的Map类型。适合存储对象,并且可以像数据库中update一个属性一样只修改某一项属性值。适用于:存
储、读取、修改用户属性。也可以用Hash做表数据缓存

list
链表(双向链表),增删快,提供了操作某一段元素的API。适用于:最新消息排行等功能;消息队列。

set
集合。哈希表实现,元素不重复,为集合提供了求交集、并集、差集等操作。适用于:共同好友;利用唯一性,统计访问网站的所有独立ip;> 好友推荐时,根据tag求交集,大于某个阈值就可以推荐。

sorted set
有序集合。将Set中的元素增加一个权重参数score,元素按score有序排列。数据插入集合时,已经进行天然排序。适用于:排行榜;带权重的消息队列。

三、使用Redis的StringRedisTemplate

3.1 添加缓存

    @Resource
    private StringRedisTemplate stringRedisTemplate;
 /**
     * 添加缓存逻辑
     * @return
     */
//    @Cacheable(value = "category",key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
    
    
        Map<String, List<Catelog2Vo>> result = null;
       // 1、加入缓存
        String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
        if(StringUtils.isEmpty(catelogJSON)){
    
    
            // 如果缓存中有就用缓存的,如果没有就从数据库查,查出后把数据放入缓存
            result = getCatalogJsonFromDB();
            // 缓存中一般都时村json,因为它可以跨语言,跨数据
            stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(result));
        }else{
    
    
            result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){
    
    });
        }
        return result;
    }

3.2 .OutOfDirectMemoryError 堆外内存移除的异常

Caused by: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
	at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:725)
	at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:680)
	at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:772)
	at io.netty.buffer.PoolArena$DirectArena.newUnpooledChunk(PoolArena.java:762)
	at io.netty.buffer.PoolArena.allocateHuge(PoolArena.java:260)
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:232)
	at io.netty.buffer.PoolArena.reallocate(PoolArena.java:400)
	at io.netty.buffer.PooledByteBuf.capacity(PooledByteBuf.java:119)
	at io.netty.buffer.AbstractByteBuf.ensureWritable0(AbstractByteBuf.java:303)
	at io.netty.buffer.AbstractByteBuf.ensureWritable(AbstractByteBuf.java:274)
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1111)
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1104)
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1095)
	at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:554)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1421)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:697)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:632)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:549)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:511)
	at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:918)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	... 1 more

io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 46137344 byte(s) of direct memory (used: 58720256, max: 100663296)
需要 allocate 46137344 物理内存 (使用了: 58720256, 最大: 100663296),100m-58m<42m 不够用造成的
这里时100m时lettuce再底层拿的jvm设置的-Xmx100m 那个数据,由于lettuce底层的bug,它不能及时回收,所以不报关你设置多大,压测时都会报OutOfDirectMemoryError
原因分析:
//1)、springboot2.0以后默认使用lettuce操作redis的客户端,它使用通信
//2)、lettuce的bug导致netty堆外内存溢出 可设置:-Dio.netty.maxDirectMemory ,但是在高并发下还是无法解决问题,因为它并不会及时的释放连接,
**解决方案:**不能直接使用-Dio.netty.maxDirectMemory去调大堆外内存
//1)、升级lettuce客户端。 2)、切换使用jedis
当前lettuce的版本是
在这里插入图片描述

两种方案的比较
lettuce 和 jedis 时底层对redis进行操作的客户端,而spring boot中redistemplete时在再一次封装了lettuce 和jedis 。
升级lettuce客户端 lettuce使用的netty作为服务器,吞吐量会更高,性能更好
jedis 客户端好久没有更新了。

当前我们的解决方案是切换使用jedis ,等以后再用升级lettuce客户端的方式取实现。

3.3 切换使用jedis操作redis 以避免压测时的OutOfDirectMemoryError

第一步:再pom中引入jedis并

<!--        引入jredis-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

第二步:在pom中排除io.lettuce

<!--        redis做缓存-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

四、缓存失效问题

4.1 缓存穿透-查询数据库中没有的数据

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次
请求都要到存储层去查询,失去了缓存的意义。要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。在流量大时,可能 DB 就挂掉了。

解决:
场景一:缓存空结果、并且设置短的过期时间

4.2 缓存雪崩-缓存大面积失效

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失
效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
例如:我们有100万的商品数据,设置了相同的过期时间,时间到了以后这100万的商品没有缓存了,此时突然有100万的并发过来,正好是请求这100万个商品的,就会出现缓存大面积失效而直接请求数据库的情况。
解决
原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的
重复率就会降低,就很难引发集体失效的事件。

4.3 缓存击穿-某一个key生效,但是这个key正好是一个热点key

对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,
是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所
有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
在这里插入图片描述
解决:
加锁

五、单体应用解决缓存失效问题

首先我们使用本地锁(synchronized或者JUC中的lock)解决,这种方案适用于单体应用。
本地锁没有把加入缓存的方法放在锁中导致的错误示例
在这里插入图片描述

 @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
    
    
        Map<String, List<Catelog2Vo>> result = null;
       // 1、加入缓存
        String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
        if(StringUtils.isEmpty(catelogJSON)){
    
    
            // 如果缓存中有就用缓存的,如果没有就从数据库查,查出后把数据放入缓存
            result = getCatalogJsonFromDB();
            // 缓存中一般都时存放json,因为它可以跨语言,跨数据
            if(result==null){
    
    
                // 1.防止缓存穿透 添加空数据设置过期时间
                stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString("{}"),60, TimeUnit.SECONDS);
            }else{
    
    
                // 2.防止缓存雪崩 添加随机过期时间
                stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(result),new Random(5).nextLong(),TimeUnit.MINUTES);
            }

        }else{
    
    
            result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){
    
    });
        }
        return result;
    }

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
    
    
        System.out.println("查询了数据库");
        synchronized (this){
    
    
            // 得到锁以后,我们应该去缓存中再确认一次,如果没有才需要继续查询
            String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
            if(!StringUtils.isEmpty(catelogJSON)){
    
    
                Map<String, List<Catelog2Vo>>  result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){
    
    });
                return result;
            }
            //将数据库的多次查询变为一次
            List<CategoryEntity> selectList = this.baseMapper.selectList(null);

            //1、查出所有分类
            //1、1)查出所有一级分类
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //封装数据
            Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
    
    
                //1、每一个的一级分类,查到这个一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

                //2、封装上面的结果
                List<Catelog2Vo> catelog2Vos = null;
                if (categoryEntities != null) {
    
    
                    catelog2Vos = categoryEntities.stream().map(l2 -> {
    
    
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());

                        //1、找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());

                        if (level3Catelog != null) {
    
    
                            List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
    
    
                                //2、封装成指定格式
                                Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());

                                return category3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(category3Vos);
                        }

                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }

                return catelog2Vos;
            }));

            return parentCid;
        }

    }

★本地锁的正确写法
在这里插入图片描述

  @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
    
    
        Map<String, List<Catelog2Vo>> result = null;
       // 1、加入缓存
        String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
        if(StringUtils.isEmpty(catelogJSON)){
    
    
            // 如果缓存中有就用缓存的,如果没有就从数据库查,查出后把数据放入缓存
            result = getCatalogJsonFromDB();
        }else{
    
    
            result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){
    
    });
        }
        return result;
    }
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
    
    
        System.out.println("查询了数据库");
        synchronized (this){
    
    
            // 得到锁以后,我们应该去缓存中再确认一次,如果没有才需要继续查询
            String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
            if(!StringUtils.isEmpty(catelogJSON)){
    
    
                Map<String, List<Catelog2Vo>>  result = JSON.parseObject(catelogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){
    
    });
                return result;
            }
            //将数据库的多次查询变为一次
            List<CategoryEntity> selectList = this.baseMapper.selectList(null);

            //1、查出所有分类
            //1、1)查出所有一级分类
            List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

            //封装数据
            Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
    
    
                //1、每一个的一级分类,查到这个一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());

                //2、封装上面的结果
                List<Catelog2Vo> catelog2Vos = null;
                if (categoryEntities != null) {
    
    
                    catelog2Vos = categoryEntities.stream().map(l2 -> {
    
    
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());

                        //1、找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());

                        if (level3Catelog != null) {
    
    
                            List<Catelog2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
    
    
                                //2、封装成指定格式
                                Catelog2Vo.Category3Vo category3Vo = new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());

                                return category3Vo;
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(category3Vos);
                        }

                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }

                return catelog2Vos;
            }));

            // 缓存中一般都时存放json,因为它可以跨语言,跨数据
            if(parentCid==null){
    
    
                // 1.防止缓存穿透 添加空数据设置过期时间
                stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString("{}"),60, TimeUnit.SECONDS);
            }else{
    
    
                // 2.防止缓存雪崩 添加随机过期时间
                stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(parentCid),(new Random(1).nextInt(5)+1),TimeUnit.MINUTES);
            }
            return parentCid;
        }

    }

★六、 分布式锁解决缓存失效的问题。

为什么要用分布式锁?
在分布式应用中,我们的某一个服务不是部署一份,而是部署多份在不同的服务器中的,本地锁只能锁住它自己的服务的代码,而无法所锁主其他微服务中的代码。

6.1 分布式锁的原理

在这里插入图片描述

6.2 使用redis的占锁功能来实现分布式锁的底层代码

redis底层方法
SET key value [EX seconds] [PX milliseconds] [NX|XX]
参数:
EX seconds – 设置键key的过期时间,单位时秒
PX milliseconds – 设置键key的过期时间,单位时毫秒
★NX – 只有键key不存在的时候才会设置key的值
XX – 只有键key存在的时候才会设置key的值

127.0.0.1:6379> set lock hahaha NX
OK
127.0.0.1:6379> set lock hahaha NX
(nil)

6.3 分布式锁的错误写法:容易造成死锁

在这里插入图片描述

    /**
     * 使用redis实现分布式锁 - 错误写法啊
     * 容易造成死锁现象
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLockError() {
    
    
        //1、占分布式锁。去redis占坑     
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock");
        if (lock) {
    
    
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            //加锁成功...执行业务
            dataFromDb = getDataFromDb();
            //删除我自己的锁
             stringRedisTemplate.delete("lock");
            return dataFromDb;
        } else {
    
    
            System.out.println("获取分布式锁失败...等待重试...");
            //加锁失败...重试机制
            //休眠一百毫秒
            try {
    
     TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
            // 调用自己重新尝试
            return getCatalogJsonFromDBWithRedisLockError();
        }
    }

6.4 分布式锁的正确写法:避免死锁

修改步骤:

  1. 为每个请求设置不同的锁值,uuid
  2. 为每个锁设置过期时间,防止死锁
  3. 设置过期时间和添加锁时原子性的
  4. 捕捉业务中的异常,在finally中删除锁
  5. 删除锁代码是原子性的。

在这里插入图片描述

 /**
     * 使用redis实现分布式锁进行数据库查询
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() {
    
    
        //1、占分布式锁。去redis占坑      设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
        String uuid = UUID.randomUUID().toString();
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
        if (lock) {
    
    
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            try {
    
    
                //加锁成功...执行业务
                dataFromDb = getDataFromDb();
            } finally {
    
    
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

                //删除锁
                stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

            }
            //先去redis查询下保证当前的锁是自己的
            //获取值对比,对比成功删除=原子性 lua脚本解锁
            // String lockValue = stringRedisTemplate.opsForValue().get("lock");
            // if (uuid.equals(lockValue)) {
    
    
            //     //删除我自己的锁
            //     stringRedisTemplate.delete("lock");
            // }

            return dataFromDb;
        } else {
    
    
            System.out.println("获取分布式锁失败...等待重试...");
            //加锁失败...重试机制
            //休眠一百毫秒
            try {
    
     TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
            return getCatalogJsonFromDBWithRedisLock();
//            return getCatalogJsonFromDbWithRedisLock();     //自旋的方式
        }

    }

原理图:
在这里插入图片描述

★七、分布式锁redission的使用

分布式锁与juc中的所有锁用法都是一样的。
上面的6.4 虽然能够为我们解决分布式锁的问题,但是官方已经不推荐我们这样使用,redission完全可以解决分布式锁的问题的,而且它功能强大,以后所有的分布式我们都会用这个工具去完成。是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)
redission 底层也是调用的redis的方法。
redis中文文档:http://www.redis.cn/
redission官网:https://github.com/redisson/redisson
redission中文文档:https://github.com/redisson/redisson/wiki/Table-of-Content
在这里插入图片描述

7.1 正式微服务整合redission

https://mvnrepository.com/search?q=Redisson
第一步:导入依赖

        <!-- 以后使用Redisson作为所有分布式锁 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>

第二步:配置redisson,此处我们用的是程序化配置
在这里插入图片描述

@Configuration
public class MyRedissonConfig {
    
    

    /**
     * 所有对Redisson的使用都是通过RedissonClient
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
    
    
        //1、创建配置
        Config config = new Config();
        // 使用单节点的redis
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        // rediss 安全连接
        // config.useSingleServer().setAddress("rediss://192.168.56.10:6379");
        //2、根据Config创建出RedissonClient实例
        //Redis url should start with redis:// or rediss://
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

第三步:测试

	@Autowired
	RedissonClient redissonClient;
	@Test
	public void redisson(){
    
    
		System.out.println("maruis------>" + redissonClient);
	}

可以拿到客户端,所以整合成功
在这里插入图片描述

7.2 分布式锁最佳实战

在这里插入图片描述

 @Autowired
    private RedissonClient redisson;
    @ResponseBody
    @GetMapping(value = "/helloredisson")
    public String helloRedisson() {
    
    

        //1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock myLock = redisson.getLock("my-lock");

        //2、加锁
        //myLock.lock();      //阻塞式等待。默认加的锁都是30s
        //1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题

         myLock.lock(30,TimeUnit.SECONDS);   //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
        //问题:在锁时间到了以后,不会自动续期
            //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
            //2、如果我们未指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
            //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
            // internalLockLeaseTime 【看门狗时间30】 / 3 = 10s
        // 3、最佳实战
        // 实战中我们还是应该给锁加上过期时间,可以把锁的时间加大一点如30s,虽然这样有死锁的风险,但是如果一个业务30s还执行不完本身就有问题的
        // 加上过期时间是为了不让看门狗自动续期,因为这样会降低程序的部分性能
        try {
    
    
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            try {
    
     TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        } catch (Exception ex) {
    
    
            ex.printStackTrace();
        } finally {
    
    
            //3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁,答案是不会因为锁默认有过期时间30s
            System.out.println("释放锁..." + Thread.currentThread().getId());
            myLock.unlock();
        }
        return "hello";
    }

7.3 分布式锁之—读写锁

读锁,和写锁一定是成对出现的
写锁控制了读锁,写锁存在,读写就都在等待。
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有读或者写的存都必须等待

/**
     * 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
     * 写锁没释放读锁必须等待
     * 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :必须等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁。写也需要等待
     * 只要有读或者写的存都必须等待
     * @return
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping(value = "/write")
    @ResponseBody
    public String writeValue() {
    
    
        String s = "";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        RLock rLock = readWriteLock.writeLock();
        try {
    
    
            //1、改数据加写锁,读数据加读锁
            rLock.lock();
            s = UUID.randomUUID().toString();
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            ops.set("writeValue",s);
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            rLock.unlock();
        }

        return s;
    }

    @GetMapping(value = "/read")
    @ResponseBody
    public String readValue() {
    
    
        String s = "";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        //加读锁
        RLock rLock = readWriteLock.readLock();
        try {
    
    
            rLock.lock();
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            s = ops.get("writeValue");
            try {
    
     TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            rLock.unlock();
        }

        return s;
    }

注意事项
在这里插入图片描述
测试结果:
写操作
在这里插入图片描述
读操作
在这里插入图片描述

7.4 分布式锁之—闭锁 人走完关门

 /**
     * 放假、锁门
     * 1班没人了
     * 5个班,全部走完,我们才可以锁大门
     * 分布式闭锁
     */

    @GetMapping(value = "/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
    
    

        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);
        door.await();       //等待闭锁完成

        return "放假了...";
    }

    @GetMapping(value = "/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id) {
    
    
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();       //计数-1

        return id + "班的人都走了...";
    }

在这里插入图片描述
测试:
在这里插入图片描述
在这里插入图片描述

7.4 分布式锁之—信号量锁,抢停车位

/**
     * 车库停车
     * 3车位
     * 信号量也可以做分布式限流
     */
    @GetMapping(value = "/park")
    @ResponseBody
    public String park() throws InterruptedException {
    
    

        RSemaphore park = redisson.getSemaphore("park");
        park.acquire();     //获取一个信号、获取一个值,占一个车位
        boolean flag = park.tryAcquire();

        if (flag) {
    
    
            //执行业务
        } else {
    
    
            return "error";
        }

        return "ok=>" + flag;
    }

    @GetMapping(value = "/go")
    @ResponseBody
    public String go() {
    
    
        RSemaphore park = redisson.getSemaphore("park");
        park.release();     //释放一个车位
        return "ok";
    }

7.5 分布式锁之—信号量锁做限流

/**
     * 信号量也可以做分布式限流
     */
    @GetMapping(value = "/park1")
    @ResponseBody
    public String park1() throws InterruptedException {
    
    

        RSemaphore park = redisson.getSemaphore("park");
        // 查实获取锁,无法获取就返回false
        boolean flag = park.tryAcquire();
        if (flag) {
    
    
            //执行业务
            System.out.println("maruis------>" + "业务代码。。。。");
        } else {
    
    
            return "error";
        }

        return "ok=>" + flag;
    }
    @GetMapping(value = "/go1")
    @ResponseBody
    public String go1() {
    
    
        RSemaphore park = redisson.getSemaphore("park");
        park.release();     //释放一个车位
        return "ok";
    }

在这里插入图片描述

九、分布式锁-缓存一致性解决方案

 /**
     * 缓存里的数据如何和数据库的数据保持一致??
     * 缓存数据一致性
     * 1)、双写模式
     * 2)、失效模式
     * @return
     */
    @Autowired
    private RedissonClient redissonClient;
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
    
    

        //1、占分布式锁。去redis占坑
        //(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
        //RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
        //创建读锁
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");

        RLock rLock = readWriteLock.readLock();

        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
    
    
            rLock.lock();
            //加锁成功...执行业务
            dataFromDb = getDataFromDb();
        } finally {
    
    
            rLock.unlock();
        }
        //先去redis查询下保证当前的锁是自己的
        //获取值对比,对比成功删除=原子性 lua脚本解锁
        // String lockValue = stringRedisTemplate.opsForValue().get("lock");
        // if (uuid.equals(lockValue)) {
    
    
        //     //删除我自己的锁
        //     stringRedisTemplate.delete("lock");
        // }

        return dataFromDb;

    }

9.1 分布式锁-缓存一致性解决方案 双写模式存在的问题

原理,更新数据库的时候同时更新缓存
在这里插入图片描述

9.2 分布式锁-缓存一致性解决方案 失效模式存在的问题

原理,更新数据库的时候删除缓存,下次请求的时候就会从数据库获取
在这里插入图片描述

9.3 原因分析

其实这两种方案都会导致数据不一致性的问题;比如在双写模式下,两个写的请求先后打过来,处理后,在写缓存是由于网络延迟等原因导致后写的请求先写缓存,先写的请求后写入缓存,这就导致了数据不一致性,缓存中的数据不是最新的数据;再比如在失效模式下,看图可知道,当我在第二个写请求还没完成时,我去读缓存,没有读到,然后去数据库中查,当我读到之后假设第二请求还没完成,当第二个请求完成之后,删掉缓存,我再更新到缓存中,也会导致数据不一致性的问题。

针对上面的问题,我们怎么解决呢?

9.4 解决方案

如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小的,就不用考虑数据不一致的问题,缓存数据加上过期时间,每隔一段时间触发读主动更新即可
如果是菜单、商品介绍等基础数据,也可以采用canal订阅binlog的方式,数据库中信息改变,canal采集这些信息,再做些处理然后同步到redis当中即可
缓存数据+过期时间足够解决大部分业务对于缓存的要求
如果写入操作稍多的话,我们可以通过加锁的方式去保证并发读写,写写的时候排好队,保证顺序,读的时候不加锁,所以适用读写锁(业务不关心脏数据,允许临时脏数据可忽略)。
对于我们能够放入缓存的数据就不应该是实时性、数据一致性要求高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新的数据即可。我们不应该过度的设计,增加系统的复杂性,遇到那些实时性、一致性要求高的数据,就应该去查询数据库,慢点就慢点。

在这里插入图片描述
cananl是一个中间件,它可以监听mysql生成的而二进制之日,当数据库更新后去更新redis中的数据。

9.5 最佳实战

为了保证我们系统数据一致性,我们要做如下的操作

  1. 缓存的所有数据都有过期时间,数据过期下一次查询出发主动更新
  2. 读写锁的使用,加上分布式的读写锁,经常写,经常读。

十一、SpringCash做缓存

11.1 文档

官网:https://docs.spring.io/spring-framework/docs/current/reference/html/
在这里插入图片描述

在这里插入图片描述

11.2 简介

** Spring 从 3.1 开始定义了 org.springframework.cache.Cache**
和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术;CacheManager 可以使用不同的工具来管理缓存,如可以使用redis也可以使用其他。
并支持使用 JCache(JSR-107)注解简化我们开发;
Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合;
Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已
经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓
存结果后返回给用户。下次调用直接从缓存中获取。
使用 Spring 缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据

11.3 整合SpringCach+redis 做缓存

第一步:添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
    
    

    // @Autowired
    // public CacheProperties cacheProperties;

    /**
     * 配置文件的配置没有用上
     * @return
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
    
    

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // config = config.entryTtl();   将存入缓存的数据转为json再存入
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //将配置文件中所有的配置都生效
        if (redisProperties.getTimeToLive() != null) {
    
    
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
    
    
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
    
    
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
    
    
            config = config.disableKeyPrefix();
        }

        return config;
    }

}

第二步:添加配置

#使用redis作为缓存
spring.cache.type=redis

#spring.cache.cache-names=qq,毫秒为单位
spring.cache.redis.time-to-live=3600000

#如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true

#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true

自定义配置类,实现往redis中存json的要求

第三步:测试
使用@EnableCaching //开启缓存功能

@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@RefreshScope
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.atguigu.gulimall.product.dao")
@EnableCaching      //开启缓存功能
public class GulimallProductApplication {
    
    

	public static void main(String[] args) {
    
    
		ConfigurableApplicationContext run = SpringApplication.run(GulimallProductApplication.class, args);
		Map<String,Object>beans = run.getBeansOfType(Object.class);
	}

}
**@Cacheable注解添加缓存**
 /**
     * 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
     * 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
     * 默认行为
     *      如果缓存中有,方法不再调用
     *      key是默认生成的:缓存的名字::SimpleKey::[](自动生成key值)
     *      缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中
     *      默认时间是 -1:
     *
     *   自定义操作:key的生成
     *      指定生成缓存的key:key属性指定,接收一个Spel
     *      指定缓存的数据的存活时间:配置文档中修改存活时间
     *      将数据保存为json格式
     *
     *
     * 4、Spring-Cache的不足之处:
     *  1)、读模式
     *      缓存穿透:查询一个null数据。解决方案:缓存空数据
     *      缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
     *      缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
     *  2)、写模式:(缓存与数据库一致)
     *      1)、读写加锁。
     *      2)、引入Canal,感知到MySQL的更新去更新Redis
     *      3)、读多写多,直接去数据库查询就行
     *
     *  总结:
     *      常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
     *      特殊数据:特殊设计
     *
     *  原理:
     *      CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
     * @return
     */
//    @Cacheable(value = {
    
    "category"},key = "#root.method.name",sync = true)
    @Cacheable(value = {
    
    "category"}) // 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用,没有就会调用这个方法
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
    
    
        System.out.println("getLevel1Categorys........");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
                new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        System.out.println("消耗时间:"+ (System.currentTimeMillis() - l));
        return categoryEntities;
    }

十二、缓存的操作注解:

// @Cacheable #触发将数据保存到缓存的曹祖
// @CacheEvict # 删除缓存
// @CachePut # 更新缓存
// @Caching # 组合以上操作
// @CacheConfig # 在类级别共享栈

  1. 添加缓存
    @Cacheable(value = {“category”},key = “#root.method.name”,sync = true)
 /**
     * 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
     * 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
     * 默认行为
     *      如果缓存中有,方法不再调用
     *      key是默认生成的:缓存的名字::SimpleKey::[](自动生成key值)
     *      缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中
     *      默认时间是 -1:
     *
     *   自定义操作:key的生成
     *      指定生成缓存的key:key属性指定,接收一个Spel
     *      指定缓存的数据的存活时间:配置文档中修改存活时间
     *      将数据保存为json格式
     *
     *
     * 4、Spring-Cache的不足之处:
     *  1)、读模式
     *      缓存穿透:查询一个null数据。解决方案:缓存空数据
     *      缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
     *      缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
     *  2)、写模式:(缓存与数据库一致)
     *      1)、读写加锁。
     *      2)、引入Canal,感知到MySQL的更新去更新Redis
     *      3)、读多写多,直接去数据库查询就行
     *
     *  总结:
     *      常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
     *      特殊数据:特殊设计
     *
     *  原理:
     *      CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
     * @return
     */
    @Cacheable(value = {
    
    "category"},key = "#root.method.name",sync = true)
//    @Cacheable(value = {
    
    "category"}) // 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用,没有就会调用这个方法
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
    
    
        System.out.println("getLevel1Categorys........");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(
                new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        System.out.println("消耗时间:"+ (System.currentTimeMillis() - l));
        return categoryEntities;
    }
  1. 删除缓存 用于实现删除模式
    @CacheEvict(value = {“category”},key = “#root.method.name”)
//    事务注解,由于此处更新的时两张表,事务的开启实在config中的MybitsConfig配置的
    @CacheEvict(value = {
    
    "category"},key = "#root.method.name")
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
    
    
        baseMapper.updateById(category);
        // 级联更新关系表中的冗余数据categoryname
        categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
    }

删除category 这个分区下所有的注解 allEntries=true 所谓的分区再redis下其实并没有分区,只是再redis的内部给我们做的逻辑上的分区。
@CacheEvict(value = “category”,allEntries=true)
3. 组合执行
@Caching

注意:key中的值要再加一个单引号,否则缓存中的名称可能不对

    @Caching(cacheable = {
    
    @Cacheable(value = "category",key = "'aaa'"),@Cacheable(value = "category",key = "'bbb'")},put={
    
    },evict = {
    
    })
  1. @CachePut 更新缓存,用户实现双写模式
    @CachePut
 @CachePut(value = "category",key = "'aaa'")

十三、Spring-Cach缓存的不足:

 * 4、Spring-Cache的不足之处:
 *  1)、读模式
 *      缓存穿透:查询一个null数据。解决方案:缓存空数据
 *      缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题,此处加的时本地锁,一般加了本地锁就可以了。
 *      缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
 *  2)、写模式:(缓存与数据库一致)
 *      1)、读写加锁。
 *      2)、引入Canal,感知到MySQL的更新去更新Redis
 *      3)、读多写多,直接去数据库查询就行
 *

总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计

十三、缓存配置源码分析

  1. 配置类:CacheAutoConfiguration
  2. 导入的缓存的配置类:CacheConfigurationImportSelector
  3. 导入配置类的方法:CacheConfigurations.getConfigurationClass(types[i]);
  4. getConfigurationClass 中可选的缓存工具:
static {
    
    
        Map<CacheType, Class<?>> mappings = new EnumMap(CacheType.class);
        mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
        mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
        mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
        mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
        mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
        mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
        mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
        mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
        mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
        mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
        MAPPINGS = Collections.unmodifiableMap(mappings);
    }
  1. RedisCacheConfiguration.class 我们选择的是redis
  2. public RedisCacheManager cacheManager 为我们提供cachemanager
  3. 并且determineConfiguration 中可以看到如果我们有自己的配置就用自己的没有就用默认的
ate org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader) {
    
    
        if (this.redisCacheConfiguration != null) {
    
    
            return this.redisCacheConfiguration;
        } else {
    
    
            Redis redisProperties = this.cacheProperties.getRedis();
            org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
            config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
            if (redisProperties.getTimeToLive() != null) {
    
    
                config = config.entryTtl(redisProperties.getTimeToLive());
            }
  1. RedisCacheConfiguration 配置类
 private final Duration ttl;
    private final boolean cacheNullValues;
    private final CacheKeyPrefix keyPrefix;
    private final boolean usePrefix;
    private final SerializationPair<String> keySerializationPair;
    private final SerializationPair<Object> valueSerializationPair;
    private final ConversionService conversionService;
  1. RedisCacheConfiguration 配置类

八、注意事项

所有的分布式锁的用法跟juc中的锁用法是相同
锁是通过锁名来判断是不是同一把锁的,锁的粒度要尽可能小。

猜你喜欢

转载自blog.csdn.net/fen_dou_shao_nian/article/details/118096622