谷粒商城 Day07 缓存与分布式锁

Day07 缓存与分布式锁

一、答疑

1、如何开启 OpenFeign 的日志功能

loggin.level.你feign接口所在的包 : debug 就可以

例如:

image-20210911162032362

2、缓存穿透问题

Redis的穿透,如果我用随机值去攻击的话,那么上课讲的设置 null 值的方法不就没有效果了吗?

  • 每次 key 都是 uuid 的东西
  • 每次缓存没有,去查数据库,然后返回 null,再缓存
  • 既浪费了 redis,数据库还防止不住
  • 缓存 null 值已经没有作用了

解决方法:布隆过滤器(下次用)【使用少量空间,来做到判断海量数据,允许误判】

image-20210911155110577

image-20210911155143250

二、高并发下缓存失效问题

1、缓存穿透

指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义

风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃,随机 key 大量攻击

解决:null 结果缓存,并加入短暂过期时间,进阶加上布隆过滤器前置

image-20210911162808264

2、缓存击穿

  • 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。 例如:item.jd.com/777.html
  • 如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

解决:加锁

大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

image-20210911163010653

3、缓存雪崩

缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻集体失效,请求全部转发到DB,DB瞬时压力过重雪崩。

各种数据都在缓存中有
1: 300
2: 800
3: 100 xxx

解决
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

image-20210911163119858

三、分布式下如何加锁

image-20210911192502267

juc的所有锁都是本地锁,只针对当前应用有效,分布式情况锁不住

只需要让所有人用同一把锁就能锁住
场景: 数据库MySQL:   修改一条数据   1000   update xxx where id=5
锁可以是数据库。
大家都去一个地方占位,能占到说明拿到锁。这个位有东西,那就说明别人占用了锁,我们就稍等
最快的就是redis
1、test_lock    id   value
                1    xxxxx
2、minio。存一个同名文件。只要我存进来,别人一看有就不存了。
3、分布式中间件,谁都能占坑。mq。create queue。
synchronized (ItemServiceImpl.class){
    //效果一样,Spring原因,this已经是单例锁了。
    //同一个jvm上部署
    //分布式下。应用在多个机器
}

四、分布式锁演进

1、基本原理

image-20210911193532481

2、分布式锁的原理演示

① 先把会话复制多份
image-20210922123719277 image-20210922134231803

4个会话都进来了

image-20210922134328276

1~3 号都没有蹲成功

image-20210922134742680

4号蹲成功了

image-20210922134818014

如果为空就set值,并返回1

如果存在(不为空)不进行操作,并返回0

3、阶段一

image-20210922220837575

那么接来下我们就结合分布式锁的原理和阶段一完成分布式锁版的获取商品详情

4、分布式锁版的获取商品详情(第一版)

① 抽取从缓存中获取商品详情的方法

queryFromCache

    /**
     * 查缓存的方法
     *
     * @param skuId
     * @return
     */
    @SneakyThrows
    public Map<String, Object> queryFromCache(Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        String redisContent = operations.get("sku:info:" + skuId);
        if (StringUtils.isEmpty(redisContent)) {
            return null;
        } else {
            return mapper.readValue(redisContent, new TypeReference<Map<String, Object>>() {
            });
        }
    }

@SneakyThrows 注解:

它是 lombok 包下的注解 并且继承了 Throwable

作用:是为了用try{}catch{}捕捉异常,添加之后会在代码编译时自动捕获异常

② 抽取把商品详情保存到缓存的方法

saveToCache

    @SneakyThrows
    void saveToCache(Map<String, Object> date, Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        String jsonStr = mapper.writeValueAsString(date);
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        //null也要缓存
        operations.set("sku:info:" + skuId, jsonStr);
    }
③ 分布式锁版本的 ItemServiceImpl (未加锁)
@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
    
    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;
    
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    
	@Override
    public Map<String, Object> getSkuInfo(Long skuId) {
        return getSkuInfoWithRedisLock01(skuId);
    }
    
    /**
     * 1、分布式锁版的获取商品详情
     */
    @SneakyThrows
    public Map<String, Object> getSkuInfoWithRedisLock01(Long skuId){
        // 1、先判断缓存中是否存在
        Map<String, Object> cache = queryFromCache(skuId);
        if (cache != null) {
            // 2、缓存中有,就用缓存的
            return cache;
        } else {
            // 3、缓存中没有调用业务逻辑真正查询
            HashMap<String, Object> feign = getFromServiceItemFeign01(skuId);
            
            // 4、查询到数据后放入缓存
            saveToCache(feign,skuId);
            return feign;
        }
    }
    
    
	/**
     * 远程查询sku详细信息(01版)
     *
     * @param skuId
     * @return
     */
    private HashMap<String, Object> getFromServiceItemFeign01(Long skuId) {
        log.info("开始远程查询,远程会操作数据库-------");

        HashMap<String, Object> result = new HashMap<>();
        //skuInfo信息

        //RPC 查询  skuDetail
        // 1、Sku基本信息(名字,id,xxx,价格,sku_描述) sku_info
        // 2,Sku图片信息(sku的默认图片[sku_info],sku_image[一组图片
        SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
        if (skuInfo != null) {
            result.put("skuInfo", skuInfo);

            // 3,Sku分类信息(sku_info[只有三级分类],根据这个三级分类查出所在的一级,二级分类内容,连上三张分类表继续查)
            BaseCategoryView skuCategorys = skuInfoFeignClient.getCategoryView(skuInfo.getCategory3Id());
            result.put("categoryView", skuCategorys);

            // 4,Sku销售属性相关信息(查出自己的sku组合,还要查出这个sku所在的spu定义了的所有销售属性和属性值)
            List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
            result.put("spuSaleAttrList", spuSaleAttrListCheckBySku);

            // 5,Sku价格信息(平台可以单独修改价格,sku后续会放入缓存,为了回显最新价格,所以单独获取)
            BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
            result.put("price", skuPrice);

            // 6,Spu下面的所有存在的sku组合信息{"121|123|156":65,"122|123|111":67}
            //前端这里还需要把map转成json字符串
            Map map = skuInfoFeignClient.getSkuValueIdsMap(skuInfo.getSpuId());
            ObjectMapper mapper = new ObjectMapper();
            try {
                String jsonStr = mapper.writeValueAsString(map);
                log.info("valuesSkuJson 内容:{}", jsonStr);
                result.put("valuesSkuJson", jsonStr);
            } catch (JsonProcessingException e) {
                log.error("商品sku组合数据转换异常:{}", e);
            }
        }
        return result;
    }
    
     /**
     * 查缓存的方法
     *
     * @param skuId
     * @return
     */
    @SneakyThrows
    public Map<String, Object> queryFromCache(Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        String redisContent = operations.get("sku:info:" + skuId);
        if (StringUtils.isEmpty(redisContent)) {
            return null;
        } else {
            return mapper.readValue(redisContent, new TypeReference<Map<String, Object>>() {
            });
        }
    }
    
     /**
     * 保存到缓存的方法
     *
     * @param skuId
     * @return
     */
    @SneakyThrows
    void saveToCache(Map<String, Object> date, Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        String jsonStr = mapper.writeValueAsString(date);
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        //null也要缓存
        operations.set("sku:info:" + skuId, jsonStr);
    }
}
④ 分布式锁版本的 ItemServiceImpl (加锁)
@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
    
    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;
    
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    
    @Override
    public Map<String, Object> getSkuInfo(Long skuId) {
        return getSkuInfoWithRedisLock01(skuId);
    }
    
	/**
     * 1、分布式锁版的获取商品详情
     */
    @SneakyThrows
    public Map<String, Object> getSkuInfoWithRedisLock01(Long skuId) {

        // 1、先判断缓存中是否存在
        Map<String, Object> cache = queryFromCache(skuId);
        if (cache != null) {
            // 2、缓存中有,就用缓存的
            log.info("缓存命中");
            return cache;
        } else {
            // 3、缓存中没有调用业务逻辑真正查询
            // 3.1、为了不全放给数据库查询,就要占锁之后再查询数据库
            // 阶段一这里我们不关心 lock 的值是什么,随便写个
            log.info("开始抢锁......");
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa");
            if (aBoolean) {
                // 缓存中没有,并且占锁成功,才执行业务
                // 3.2、占锁成功
                log.info("抢占成功......");
                cache = getFromServiceItemFeign01(skuId);

                // 4、查询到数据后放入缓存
                saveToCache(cache, skuId);

                // 5、删除锁
                stringRedisTemplate.delete("lock");
            } else {
                log.info("没抢成功,开始尝试自旋占锁......");
                // 3.3、占锁失败
                // 不断地操作 redis
                while (stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa")) {
                    log.info("自旋占锁成功......");
                    // 自旋锁成功才调用自己
                    cache = getSkuInfoWithRedisLock01(skuId);

                    // 记得释放锁
                    stringRedisTemplate.delete("lock");
                }
            }
        }
        return cache;
    }
    
    /**
     * 远程查询sku详细信息(01版)
     *
     * @param skuId
     * @return
     */
    private HashMap<String, Object> getFromServiceItemFeign01(Long skuId) {
        log.info("开始远程查询,远程会操作数据库-------");

        HashMap<String, Object> result = new HashMap<>();
        //skuInfo信息

        //RPC 查询  skuDetail
        // 1、Sku基本信息(名字,id,xxx,价格,sku_描述) sku_info
        // 2,Sku图片信息(sku的默认图片[sku_info],sku_image[一组图片
        SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
        if (skuInfo != null) {
            result.put("skuInfo", skuInfo);

            // 3,Sku分类信息(sku_info[只有三级分类],根据这个三级分类查出所在的一级,二级分类内容,连上三张分类表继续查)
            BaseCategoryView skuCategorys = skuInfoFeignClient.getCategoryView(skuInfo.getCategory3Id());
            result.put("categoryView", skuCategorys);

            // 4,Sku销售属性相关信息(查出自己的sku组合,还要查出这个sku所在的spu定义了的所有销售属性和属性值)
            List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
            result.put("spuSaleAttrList", spuSaleAttrListCheckBySku);

            // 5,Sku价格信息(平台可以单独修改价格,sku后续会放入缓存,为了回显最新价格,所以单独获取)
            BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
            result.put("price", skuPrice);

            // 6,Spu下面的所有存在的sku组合信息{"121|123|156":65,"122|123|111":67}
            //前端这里还需要把map转成json字符串
            Map map = skuInfoFeignClient.getSkuValueIdsMap(skuInfo.getSpuId());
            ObjectMapper mapper = new ObjectMapper();
            try {
                String jsonStr = mapper.writeValueAsString(map);
                log.info("valuesSkuJson 内容:{}", jsonStr);
                result.put("valuesSkuJson", jsonStr);
            } catch (JsonProcessingException e) {
                log.error("商品sku组合数据转换异常:{}", e);
            }
        }
        return result;
    }
    
     /**
     * 查缓存的方法
     *
     * @param skuId
     * @return
     */
    @SneakyThrows
    public Map<String, Object> queryFromCache(Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        String redisContent = operations.get("sku:info:" + skuId);
        if (StringUtils.isEmpty(redisContent)) {
            return null;
        } else {
            return mapper.readValue(redisContent, new TypeReference<Map<String, Object>>() {
            });
        }
    }
    
     /**
     * 保存到缓存的方法
     *
     * @param skuId
     * @return
     */
    @SneakyThrows
    void saveToCache(Map<String, Object> date, Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        String jsonStr = mapper.writeValueAsString(date);
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        //null也要缓存
        operations.set("sku:info:" + skuId, jsonStr);
    }
}
(1)redis setIfAbsent 的使用

如果为空就set值,并返回1

如果存在(不为空)不进行操作,并返回0

很明显,比get和set要好。因为先判断get,再set的用法,有可能会重复set值

(2)setIfAbsent 和 setnx

setIfAbsent 是java中的方法

setnx 是 redis命令中的方法

setnx 例子

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
⑤ 打开 log 的 info 级别日志

application.yaml

logging:
  level:
    com:
      atguigu:
        gmall: info
⑥ 测试
image-20210922210218131

9000:缓存命中

image-20210922210438758

9001:百万并发进来,它先抢占成功了,然后开始远程查询数据库

image-20210922210548284

查询数据库成功以后,它就把锁释放了,因为它已经查询到了,其他人也就不用再查数据库了,直接查询缓存就有

image-20210922210847213

9002:缓存命中

image-20210922211134266

9003:缓存命中

image-20210922211215042

因为 9001 干活太快了,以至于其他三个都没有抢锁的机会

⑦ 漏洞

这个版本有漏洞

image-20210922220821253 image-20210922220635599

那么,用 try-catch-finally,把删锁代码放到 finally 中可以防止这个问题吗?

try{
    //业务代码  1000
}finally {
    stringRedisTemplate.delete("lock");
}

答案是不可以!try-catch-finally 只能保证在正常情况下有用,但是如果是一些特殊情况下就没用了,比如在执行业务代码的时候突然断电了,代码根本都执行不到 finally 中,删锁代码还是没有执行

单靠 try-catch-finally 是不够的的,害得要加上 redis 的过期时间,这样就算在执行业务代码的时候突然断电了,finally中的删锁代码没执行,但是 redis 加了过期时间,它会自己删

这就是我们的阶段二

5、阶段二

image-20210922225237221

redis 设置过期时间

image-20210922224300227
① 思考:这样就能保证万无一失了吗?
image-20210922224514647

还有可能出现这种问题,因为 redis 有两次操作,第一次是占坑,第二次是设置过期时间,中间会有空档期,万一这个空档期出问题了导致过期时间没设置上,那还是等于没设置,依然会出现锁无法释放,其他人永远抢不到锁

② 解决方法

给 redis 把话一次性说完,别分两次说。也即第一次占坑就设置过期时间,保证原子性

这就是我们的阶段三了

6、阶段三

image-20210923002045997
① 解决阶段二的问题

redis 在底层有这个命令的支持

set lock 1 NX EX 20

# setnx(set if not exist)
# setex(set expire value)
image-20210922225915144
// 底层调用 set lock 1 NX EX 20
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa",20, TimeUnit.SECONDS);
image-20210922232931668

这样一次就指定好了 redis 占坑+设置过期时间了

image-20210922235946258

② 漏洞

可能会出现超过锁的过期时间还未完成业务,此时另一个线程会侵入,因为锁的 key 是一样的,所以等第一个线程结束业务后会释放第二个线程的锁,但是这个 key 别人正在用,第二个线程的锁被释放后,第三个线程就进来了…这样就会导致至少有两个人都在运行这段代码,相当于我们的分布式锁锁了个寂寞

场景:我们设置锁的过期时间为10s,线程 A 的业务代码执行的时间就是长,用了15s才执行完,但是在第10s的时候锁的过期时间已经到了,这个锁已经被删了,但由于锁已经被删了马上就有线程B进来了,线程 B 执行到第5s的时候线程 A 已经执行完了,此时 A 就要执行删锁代码,因为用的是同一把锁,锁的 key 是一样的,因此 A 就把 B 的锁给删除了;此时 线程 C 抢到了锁进来了…

③ 解决方法

删除锁的时候用自己的值,以前我们只是占坑,根本不关心坑里面的东西(value 的值)是什么,现在我们可以整一个UUID,生成一个唯一字符串 token,然后在删锁之前将 token 和 value 进行比价,看两者是否相同,如果相同则说明是自己的锁,此时再删除;如果不相同则说明不是自己的锁,就不能删除

这个解决方法就是阶段四了

7、阶段四

image-20210923093346234 image-20210923114428822 image-20210923114517634
① 思考:这样就能保证万无一失了吗?
image-20210923124948182

归根结底还是因为获取值和删除锁是两步的,没有保证原子性

image-20210923121016603

要把这两步变成一步,保证原子性

② 解决方法

利用脚本把两步变成一步,这就是阶段五了

8、阶段五(最终形态?1)

image-20210923121510889
① 解决阶段四的问题
image-20210923121833607
// 告诉redis,看 KEYS 是否等于 ARGV(argValue) ,如果是就删除 KEYS,否则返回0
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
        "then return redis.call('del', KEYS[1]) else return 0 end";
// lua脚本原子执行
stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), token);

保证原子性的判断+删锁,并不是说判断不是自己的锁,哪怕过期时间到了,redis 都不删锁,而是保证删的是自己的锁,不删别人的锁,这点要分清楚

② 漏洞

其实这里还有个问题,假设锁的过期时间是10s,1号线程它业务逻辑多就是执行了15s,第10s的时候这个锁已经过期了,此时其他线程进来了,redis 第15s才会删锁,因为加了脚本保证了(判断+删锁)的原子性,此时的 redis 一判断发现 token 和 lock 的值 lockValue 不相等,于是 redis 也不删。但是现在出现了一个问题,同时有两个人进来了(业务场景:1号线程先进来查数据库,数据库还没查询完呢,此时2号线程就进来了,2号线程看缓存中有没有,没有就查数据库)这就又完蛋了。

所以锁的过期时间很有讲究,接来下就要谈到锁的续期时间了。也就是说要给他不断续期,业务不中断,锁就要自动延长时间

③ 锁的续期

一个分布式锁怎么能写成功

锁有自动过期时间,防止死锁

加锁解锁都是原子的

锁的续期 10s,要给他不断续期,业务不中断。锁要自动延长时间?

​ 后台启动一个 daemon 守护线程,每隔5秒,让这个锁重新开始倒计时,续期就是直接续满

​ new Thread()

​ new TimerTask(); 10

我们能想到的,框架的设计者们他们也会想到,所以就有一个叫 redisson 的基于 redis 做的分布式锁,分布式对象的框架

9、改进 getSkuInfoWithRedisLock01

结合视频 Day07 缓存与分布式锁 07、如何做一个可重入锁分布式锁

    /**
     * 分布式锁版的获取商品详情
     */
    @SneakyThrows  
	//自己设置的使用原生redis操作实现的分布式可重入锁业务
    public Map<String, Object> getSkuInfoWithRedisLock01(Long skuId) {
        //1、先判断缓存中是否存在单位时间内一个人的所有都执行完了
        System.out.println("Thread.currentThread() = " + Thread.currentThread());
        Map<String, Object> cache = queryFromCache(skuId);
        if (cache != null) {
            //2、缓存中有用缓存的
            log.info("缓存命中.....");
            return cache;
        } else {
            //3、缓存中没有调用业务逻辑真正查询
            //3.1)、为了不全放给数据库,占锁来查数据库
            log.info("缓存不命中,开始抢锁.....");
            //第一次连接告诉redis,占坑  用 "lock"
            //底层调用 set lock 1 NX EX 20
            String token = UUID.randomUUID().toString();
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 20, TimeUnit.SECONDS);
            // 还没运行断电了, 不可重入锁会死锁。  未来设计的所有锁都应该是可重入
            if (aBoolean) {
                //第二次 才设置过期时间
//                stringRedisTemplate.expire("lock",20, TimeUnit.SECONDS);
                //缓存中没有,并且占锁成功才执行业务
                //3.2)、占锁成功
                try {
                    log.info("抢占成功.....");
                    cache = getFromServiceItemFeign(skuId);
//                Thread   sleep是线程阻塞操作
                    //4、查询到数据后放入缓存
                    saveToCache(cache, skuId);  //异常
                } finally {
                    //5、删除锁,一定得执行
                    //保证业务正常出现了异常,finally兜底
                    //断电这种故障,redis过期时间,让redis自己删除
                    // 告诉redis,看 KEYS 是否等于 ARGV(argValue) ,如果是就删除 KEYS,否则返回0
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                            "then return redis.call('del', KEYS[1]) else return 0 end";
                    //lua脚本原子执行
                    stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), token);

                }
                
            } else {

                //3.3)、占锁失败
                // 不断的操作redis    非公平锁,没有占锁成功就一直抢占
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa")
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa") == true  //生产的写法
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa") == false
                // 一直抢直到占到锁,写死while(true)
                log.info("没抢成功,开始尝试自旋占锁.....");
                while (true) {
                    log.info("自旋中.....");
                    Thread.sleep(200);  // 同一线程里面锁应该直接使用
                    String token2 = UUID.randomUUID().toString();
                    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent("lock", token2,20, TimeUnit.SECONDS);
                    if (absent) {
                        // while(true) 自己调用自己,立马栈溢出
                        log.info("自旋占锁成功.....");
                        //自旋锁成功才调用自己, 栈溢出的写法,但是能看出效果
                        cache = getSkuInfoWithRedisLock01(skuId);
                        //释放锁  cpu速度远大于 redis,请用原子删锁和原子加锁

                        String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                                "then return redis.call('del', KEYS[1]) else return 0 end";
                        //lua脚本原子执行
                        stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), token2);
                        return cache;
                    }
                }
            }
        }
        return cache;
    }
① 测试

先让线程池热个身

image-20210923145913779

线程池热完身 redis 里面就有数据了,我们此时再把 redis 里面的数据 flush 掉

image-20210923150059158

然后重新来,把控制台的信息清空

我们自己单元测试,debug 方式启动,手动模拟把 lock 放进 redis

image-20210923150525517

前端发请求

image-20210923150655490

然后看后端,9000 因为没有缓存命中,开始抢锁,没抢成功正在不断自旋中

image-20210923150729123

image-20210923150832070

我们手动把 redis 中的 lock 删除,模拟让 9000 自旋占锁成功

image-20210923151121612

自旋占锁成功,又调自己,我们 step into 进去

image-20210923151325661

缓存中没有,因为我们占锁成功还没人查

image-20210923151445683

开始抢锁查数据库,这个锁就是我自己的,所以直接就是 true,占锁成功

image-20210923151545177 image-20210923151849906
② 有个 bug

这里自旋成功后,进入 getSkuInfoWithRedisLock01 方法,

image-20210923152918322

但是这个方法里面又会判断是否缓存命中,如果不命中则又要开始抢锁

image-20210923153415591

所以相当于抢了两遍,我们刚刚 debug 测试能成功,是因为在自旋的时候抢锁,但是在 cache = getSkuInfoWithRedisLock01(skuId) 时已经过了 10s 超时了,redis 删了

image-20210923153847690

如果我们在这里没有超时时间,这个锁是可重入的吗?

image-20210923154223269

如果设计为可重入锁

image-20210923154346077 image-20210923154428524

就不用抢了

我们希望这个锁能往下传递

10、改进 getSkuInfoWithRedisLock01 (第二版)

@SneakyThrows  //自己设置的使用原生redis操作实现的分布式可重入锁业务
    public Map<String, Object> getSkuInfoWithRedisLock01(Long skuId) {
        //1、先判断缓存中是否存在 单位时间内一个人的所有都执行完了
        System.out.println("Thread.currentThread() = " + Thread.currentThread());
        Map<String, Object> cache = queryFromCache(skuId);
        if (cache != null) {
            //2、缓存中有用缓存的
            log.info("缓存命中.....");
            return cache;
        } else {
            //3、缓存中没有调用业务逻辑真正查询
            //3.1)、为了不全放给数据库,占锁来查数据库
            log.info("缓存不命中,开始抢锁.....");
            //第一次连接告诉redis,占坑  用 "lock"
            //底层调用 set lock 1 NX EX 20
            String token = UUID.randomUUID().toString();
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 20, TimeUnit.SECONDS);
            //还没运行断电了, 不可重入锁会死锁。  未来设计的所有锁都应该是可重入
            if (aBoolean) {
                //第二次 才设置过期时间
//                stringRedisTemplate.expire("lock",20, TimeUnit.SECONDS);
                //缓存中没有,并且占锁成功才执行业务
                //3.2)、占锁成功
                try {
                    log.info("抢占成功.....");
                    cache = getFromServiceItemFeign(skuId);
//                Thread   sleep是线程阻塞操作
                    //4、查询到数据后放入缓存
                    saveToCache(cache, skuId);  //异常
                } finally {
                    //5、删除锁,一定得执行
                    //保证业务正常出现了异常,finally兜底
                    //断电这种故障,redis过期时间,让redis自己删除
                    //下面的删锁代码必须是原子型,否则可能出问题
//                    String lock = stringRedisTemplate.opsForValue().get("lock");
//                    if(token.equals(lock)){
//                        //删除我自己的锁
//                        stringRedisTemplate.delete("lock");
//                        System.out.println("删除分布式锁....");
//                        log.info("删除分布式锁....");
//                    }

                    // 告诉redis,看 KEYS 是否等于 ARGV(argValue) ,如果是就删除 KEYS,否则返回0
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                            "then return redis.call('del', KEYS[1]) else return 0 end";
                    //lua脚本原子执行
                    stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), token);

                }

            } else {

                //3.3)、占锁失败
                //??? 不断的操作redis    非公平锁,没有占锁成功就一直抢占
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa")
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa") == true  //生产的写法
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa") == false
                // 一直抢直到占到锁,写死while(true)
                log.info("没抢成功,开始尝试自旋占锁.....");
                while (true) {
                    log.info("自旋中.....");
                    Thread.sleep(200);  // 同一线程里面锁应该直接使用
                    String token2 = UUID.randomUUID().toString();
                    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent("lock", token2);
                    if (absent) {
                        // while(true) 自己调用自己,立马栈溢出
                        log.info("自旋占锁成功.....");
                        //自旋锁成功才调用自己, 栈溢出的写法,但是能看出效果
                        //这个方法能进去抢占成功,是因为absent已经超时了,redis删了
                        //锁设计为可重入锁
                        cache = getSkuInfoWithRedisLock01(skuId);
                        //释放锁  cpu速度远大于 redis,请用原子删锁和原子加锁

                        String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                                "then return redis.call('del', KEYS[1]) else return 0 end";
                        //lua脚本原子执行
                        stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), token2);
                        return cache;
                    }
                }
            }
        }

        return cache;
    }

自旋的时候不加超时时间

image-20210923155613806
① 测试 + 分析

先把 redis 中的数据删掉

image-20210923155734914

为了让它们抢占锁,我们手动在 redis 中加一个锁

image-20210923155825849

什么叫可重入锁?

// 伪代码
A {
    // A 调用 B 加了锁
    B {
        // B 方法说我也要加这把锁
    }
}

如果设计为不可重入的:A 因为有了这个锁,B 想要加 A 这把锁,它就一直加不上,必须等 A 释放,但是 A 怎么释放呢?A 还等着 B 执行完才能释放,形成两个方法都要用同一把锁,所以锁必须到设计为可重入的
    
所谓可重入锁,就是 A 的锁,B 可以直接拿过来用,BA 占了就直接拿过来用,反正 B 是自旋过来的,肯定要直接用

进来先查缓存中有没有,发现缓存中没有

image-20210923161032901

缓存不命中,开始抢锁

image-20210923161122386

它没抢到锁,因为我们在上面通过手动在 redis 中添加了锁,这里肯定是抢不到的

image-20210923161211319

抢不到开始自旋抢锁

image-20210923161346502

假设这次自旋抢到了(我们手动把 redis 中的 lock 删除)

image-20210923161448300

抢到了,锁是 0afe3a5b-a68b-XXXX

image-20210923161520194 image-20210923161639361

抢成功了,继续执行业务逻辑,step into 进来看锁能不能可重入

image-20210923161725313

先查缓存,缓存中没有

image-20210923173823839

开始抢锁,又生成了一个 token

image-20210923173920613

这个新生成的 token 与 redis 中的 lock 的值不相同

image-20210923174030300

不相同,肯定是占锁失败,而且我明确的告诉你会一直失败,因为上一个人进来这个锁已经帮你占了,锁重入直接进来就行了

image-20210923174144391

现在就是这么个情况

自旋尝试占锁了,而且都已经帮你占好锁了

image-20210923174855073

然后进入方法里面,你直接用就可以了

image-20210923174919160

但是现在发现这块用不到,还要抢锁,但是 token 与 redis 中的有不同,所以一致抢锁失败

image-20210923174955862

我们现在的锁唯一的问题就是非重入的,上面抢占失败就得自旋,自旋继续占锁,占锁失败又得自旋…,其实在第一次自旋的时候都已经帮你占好锁了,占好以后我希望你以后直接使用

也就是同一线程里面锁应该直接使用

我们把 getSkuInfoWithRedisLock01 改成可重入锁版

11、getSkuInfoWithRedisLock01(可重入锁版)

① 改进代码

在 getSkuInfoWithRedisLock01() 中再加两个参数:Boolean locked :是否锁定了,String beforeToken 如果锁定了它用的 token 是什么

image-20210923175819664

第一次调用 getSkuInfoWithRedisLock01 时没有锁定,token 是 null

image-20210923180137858

当自旋,占锁成功以后,就把占锁成功的 locked 和 token 往下传

image-20210923180420069
	@Override
    public Map<String, Object> getSkuInfo(Long skuId) {
        return getSkuInfoWithRedisLock01(skuId,false,null);
    }


	@SneakyThrows  //自己设置的使用原生redis操作实现的分布式可重入锁业务
    public Map<String, Object> getSkuInfoWithRedisLock01(Long skuId, Boolean locked, String beforetToken) {

        // while ()  不能上来就用锁
        //1、先判断缓存中是否存在 单位时间内一个人的所有都执行完了
        System.out.println("Thread.currentThread() = " + Thread.currentThread());
        Map<String, Object> cache = queryFromCache(skuId);
        if (cache != null) {
            //2、缓存中有用缓存的
            log.info("缓存命中.....");
            return cache;
        } else {
            //3、缓存中没有调用业务逻辑真正查询
            //3.1)、为了不全放给数据库,占锁来查数据库
            log.info("缓存不命中,开始抢锁.....");
            //第一次连接告诉redis,占坑  用 "lock"
            //底层调用 set lock 1 NX EX 20
            String token = beforetToken;
            Boolean aBoolean = locked;
            if (!locked) {  //判断之前有人已经帮我锁定了,我就直接用别人的锁
                token = UUID.randomUUID().toString();  //自己加锁就是新token
                //重入锁的设计
                aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 20, TimeUnit.SECONDS);
            }
            //还没运行断电了, 不可重入锁会死锁。  未来设计的所有锁都应该是可重入
            if (aBoolean) {
                //第二次 才设置过期时间
//                stringRedisTemplate.expire("lock",20, TimeUnit.SECONDS);
                //缓存中没有,并且占锁成功才执行业务
                //3.2)、占锁成功
                try {
                    log.info("抢占成功.....");
                    cache = getFromServiceItemFeign(skuId);
//                Thread   sleep是线程阻塞操作
                    //4、查询到数据后放入缓存
                    saveToCache(cache, skuId);  //异常
                } finally {
                    //5、删除锁,一定得执行
                    //保证业务正常出现了异常,finally兜底
                    //断电这种故障,redis过期时间,让redis自己删除\
                    //下面的删锁代码必须是原子型,否则可能出问题
//                    String lock = stringRedisTemplate.opsForValue().get("lock");
//                    if(token.equals(lock)){
//                        //删除我自己的锁
//                        stringRedisTemplate.delete("lock");
//                        System.out.println("删除分布式锁....");
//                        log.info("删除分布式锁....");
//                    }

                    // 告诉redis,看 KEYS 是否等于 ARGV(argValue) ,如果是就删除 KEYS,否则返回0
                    String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                            "then return redis.call('del', KEYS[1]) else return 0 end";
                    //lua脚本原子执行
                    stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), token);

                }
//                try{
//                    //业务代码  1000
//                }finally {
//                    stringRedisTemplate.delete("lock");
//                }

            } else {

                //3.3)、占锁失败
                //??? 不断的操作redis    非公平锁,没有占锁成功就一直抢占
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa")
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa") == true  //生产的写法
                // stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa") == false
                // 一直抢直到占到锁,写死while(true)
                log.info("没抢成功,开始尝试自旋占锁.....");
                while (true) {
                    log.info("自旋中.....");
                    Thread.sleep(200);  // 同一线程里面锁应该直接使用
                    String token2 = UUID.randomUUID().toString();
                    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent("lock", token2);
                    if (absent) {
                        // while(true) 自己调用自己,立马栈溢出
                        log.info("自旋占锁成功.....");
                        //自旋锁成功才调用自己, 栈溢出的写法,但是能看出效果
                        //这个方法能进去抢占成功,是因为absent已经超时了,redis删了
                        //锁设计为可重入锁
                        cache = getSkuInfoWithRedisLock01(skuId, absent, token2);
                        //释放锁  cpu速度远大于 redis,请用原子删锁和原子加锁

                        String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                                "then return redis.call('del', KEYS[1]) else return 0 end";
                        //lua脚本原子执行
                        stringRedisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), token2);
                        return cache;
                    }
                }
            }
        }

        return cache;
    }
④ 测试

前端发请求

image-20210923181948411

第一次进来 locked 是 false,token 是 null

image-20210923182022268

先查缓存中有没有,缓存中没有

image-20210923182118186

缓存中没有准备加锁查,先判断别人有没有已经帮我锁定的,发现别人没有帮我锁定 locked 是 false

image-20210923182313028

所以进来自己加锁,自己加的锁 token 是 bc20ceea-d4cf-47a8-783cacfe7e3f

image-20210923182639534

redis 中的 lock 值

image-20210923182830285

这个锁加不成功,因为 redis 中已经有了

image-20210923182914453

加锁失败开始自旋,不断抢

image-20210923182956749

当我们将 redis 中的 lock 手动删除后

image-20210923183057763

生成了一个新的 token2 ,自旋占锁成功

image-20210923183219278

此时再看 redis

image-20210923183321274

继续往下执行,要调用 getSkuInfoWithRedisLock01 方法了,会把自旋占锁的结果(true,fbed2XXX)传过来

image-20210923183449133

step into 进入 getSkuInfoWithRedisLock01 方法,进来先查缓存中有没有,缓存中没有

image-20210923183859008

因为前人已经帮我占好锁了(前人栽树后人乘凉),我把前人的 token(fbedXXX) 和 locked(true)拿来,然后判断 locked

image-20210923184039977

前人已经帮我占好锁了,我都不用锁,直接就抢占成功了

image-20210923184438007

执行业务代码:查询数据库,将数据放入缓存

image-20210923184734821

业务代码执行完后准备删锁

image-20210923184922255

因为是上一次重入的人给我的锁,所以我删的是 fbedXXXXX,没毛病

image-20210923184955770

至此,所有的逻辑已经走完

⑤ 小总结

不可重入锁会死锁,未来设计的所有锁都应该是可重入锁

我们写个可重入锁写得这么难受,那有没有已经封装好的框架呢?唉,于是乎我们的 redisson 它就来了

五、redisson

1、概念

redisson: redis+son

redisson 和 redis 的关系

redis:中间件

redisson:操作redis的客户端, 比:jedis、stringRedisTemplate强大

2、service-item 做好 redisson 的配置

redisson 配置的参考地址

https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95#22-%E6%96%87%E4%BB%B6%E6%96%B9%E5%BC%8F%E9%85%8D%E7%BD%AE

① pom.xml
<!-- 引入分布式锁客户端 -->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.3</version>
</dependency>
② ItemServiceRedissonConfig

参考资料:

image-20210923191104167

新建 com.atguigu.gmall.item.config.ItemServiceRedissonConfig

/**
 * matchIfMissing = true: 没配置就是true,不配就是默认生效
 *
 * @ConditionalOnProperty: 配置文件中有指定的 prefix.name 属性,、
 * 并且值是 havingValue 指定的,则@Configuration 生效。
 *
 * 1、引入redisson依赖
 * 2、给容器中放一个 RedissonClient,以后用他操作redis。
 *
 * redisson 做出来的锁,API都和JUC一样。
 * JUC是本地锁;读写锁、信号量、可重入锁
 * Redisson是分布式锁;
 */
@ConditionalOnProperty(prefix = "redisson", name = "enable",
        havingValue = "true", matchIfMissing = true)
@Configuration
public class ItemServiceRedissonConfig {

//    Lock lock = new ReentrantLock();  对象一样是同一把锁

    /**
     * 给容器中放一个操作redis的客户端,redisson工具
     * 1、这个方法的参数 RedisProperties ,Spring会自动从容器中获取
     *
     * @return
     */
    @Bean
    public RedissonClient redissonClient(RedisProperties redisProperties) {
//        RedissonClient redisson = Redisson.create();   //连接本地redis

        String host = redisProperties.getHost();
        int port = redisProperties.getPort();
        String password = redisProperties.getPassword();
        // redis:// or rediss://
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port)
                .setPassword(password)
        ;

//        long timeout = config.getLockWatchdogTimeout();   //看门狗时间用来自动续期
        RedissonClient redisson = Redisson.create(config);

        return redisson;
    }
}
③ application.yaml

测试

RedisTempalteTest

@Test
void redissonClient(){
    System.out.println(redissonClient);
}
image-20210923194214195

3、可重入锁-lock()

image-20210923211802121
// 只要 key 一样就说明是同一把锁,也就是下面的 "lock"
RLock lock = redissonClient.getLock("lock");
① ItemServiceImpl(redisson版)
@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
	@Autowired
    SkuInfoFeignClient skuInfoFeignClient;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;
    
    /**
    * 使用redisson分布式锁做的方法
    * @param skuId
    * @return
    */
    @SneakyThrows
    public Map<String, Object> getSkuInfoWithRedissonDistributeLock(Long skuId) {
        log.info("准备查询" + skuId + "号数据");
        //这段代码是加锁的吗?不是,这段代码只是拿到锁
        RLock lock = redissonClient.getLock("lock");
        
        /**
         * locked:默认传false
         * beforeToken:默认传null
         * 原生方式
         * return getSkuInfoWithRedisLock01(skuId,false,null); 
         */
        
        //2、redisson版
        Map<String, Object> cache = queryFromCache(skuId);
        if (cache == null) {
            log.info("redis缓存没命中...准备查询数据库 ");

            //3、加锁
            try {
                // 看门狗,用来自动续期,redisson的所有功能都是原子性的
                // 这是一个自旋,可以续期,锁的默认时间是30 * 1000 ; 30s
                lock.lock(); 
                // 每隔10s定时任务会自动续满期 internalLockLeaseTime / 3,
                // lock.lock(30,TimeUnit,SECONDS); //30秒以后自动解锁,不会自动续期
                log.info("加锁成功...尝试继续命中缓存...");
                Map<String, Object> cacheAgain = queryFromCache(skuId);
                if (cacheAgain == null) {
                    log.info("开始查询数据库...");
                    HashMap<String, Object> date = getFromServiceItemFeign(skuId);
                    saveToCache(date, skuId);
                    return date;
                }
                log.info("命中缓存,直接返回...");
                return cacheAgain;
            } finally {
                //redisson 感知到解的不是自己的锁,然后就会抛出异常
                lock.unlock();
            }
        }
        log.info("缓存命中,直接返回");
        return cache;
    }
}
② jmeter 测试

添加一个线程组

image-20210923223748818 image-20210923223904760

添加监听器—汇总报告

image-20210923223940168

添加监听器—聚合报告

image-20210923224025183

线程组添加取样器—HTTP请求

image-20210923224123054

image-20210923224306315

启动

image-20210923224339162

查看汇总报告

image-20210923224411436

查看聚合报告

image-20210923224430971

如果要重新测试的话,得先清除报告

image-20210923224540540

③ Console 结果

9000:

image-20210923224647206 image-20210923224744570

9001:只有9001查询了一遍数据库,注意线程号,线程号相等的是一组

image-20210923225920668

image-20210923225243542

image-20210923225321169

为什么打印这么多的加锁成功...尝试继续命中缓存?因为上一个人加锁成功后尝试查缓存,缓存中有就直接返回,这个时候就把锁给释放了,下一个人就加锁成功了

ab 测试不准,以后压力测试就用 jmeter

4、redisson 使用小结 & 面试题

① redisson 会死锁吗?

答:不会,因为 redisson 是可重入锁(同一线程内前面加过后面不用加)

(1)redisson 会自动解锁吗?

如果 finally 中的解锁代码 lock.unlock(); 没有调到,比如说突然断电了,redisson 会自动解锁吗?

image-20210923230553945

答案是:会

看门狗30s,代码闪断,定时(守护)线程没有了,就不自动续期了,等30s,redis自己删除,默认加锁就是30s

(2)推荐不要使用 lock(30,TimeUnit.SECONDS);

因为如果这样设置了,就会失去了自动续期功能

scheduleExpirationRenewal(threadId); 的调用前提是 if (leaseTime == -1) 没有续期功能;

lock() 源代码:

image-20210923231725544 image-20210923232340667 image-20210923232450726 image-20210923232710582 image-20210923233317797

发现设置了过期时间后就会自动续期了

(3)看门狗

看门狗只是为了当业务超时,锁时间不够来不停续期的

看门狗还有默认的30秒(lock.lock())自动过期(redis删),就是防止特殊情况

lock.lock() 的看门狗只是看当前线程,默认过期时间是30s,当前线程哪怕执行一秒就结束了,我们这看门狗也就结束了;当前线程一直在,看门狗就一直在

② redisson 分布式业务超时怎么办?

答:看门狗自动续期,每隔1/3看门狗时间就续满看门狗时间

5、可重入锁-trylock()

image-20210924001332800

Redisson同时还为分布式锁提供了异步执行的相关方法:

RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
① 同步加锁
image-20210924002128253
② 异步加锁
image-20210924002750016

6、读写锁(ReadWriteLock)

① 介绍

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
② LockTestController

新建 com.atguigu.gmall.item.controller.LockTestController

@RestController
public class LockTestController {

    @Autowired
    RedissonClient redissonClient;

    @Autowired
    StringRedisTemplate redisTemplate;

    @SneakyThrows
    @GetMapping("/write")
    public String readWrite(){

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");

        RLock lock = readWriteLock.writeLock();

        lock.lock();

        String s = UUID.randomUUID().toString();
        Thread.sleep(10000);
        redisTemplate.opsForValue().set("mymsg", s);
        lock.unlock();


        return s;
    }
    
    @GetMapping("/read")
    public String read(){
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");

        RLock lock = readWriteLock.readLock();

        lock.lock();
        String mymsg = redisTemplate.opsForValue().get("mymsg");
        lock.unlock();

        return mymsg;
    }
}
③ 测试

起两台机器

image-20210924003647233
(1)并发写

前端发请求

image-20210924003747309

等第一个人写锁释放后,第二个人才能获得写锁

image-20210924003900292

过了几秒后

image-20210924003944473

(2)并发读

并发读是无锁的

image-20210924004045507

(3)一写一读

写的时候还想要读,读就被阻塞了

image-20210924004205214

只有写锁释放了,才能读

image-20210924004328410

7、信号量(Semaphore)

① 介绍

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
② LockTestController
    @GetMapping("/inittcc")
    public String tcc(int size){

        RSemaphore whsggdxtcc = redissonClient.getSemaphore("whsggdxtcc");
        whsggdxtcc.addPermits(size);
        return "whsggdxtcc 停车场有 "+size + "  个车位";
    }

    @GetMapping("/stopcar")
    public String stop() throws InterruptedException {
        RSemaphore whsggdxtcc = redissonClient.getSemaphore("whsggdxtcc");
        whsggdxtcc.acquire(1);  //从信号量里面拿一个
        return "停车成功...";
    }

    @GetMapping("/gocar")   //信号量
    public String start(){
        RSemaphore whsggdxtcc = redissonClient.getSemaphore("whsggdxtcc");
        whsggdxtcc.release(1);   //给信号量加值
        return "车开走了...";
    }

③ 测试
(1)初始化信号量
image-20210924005330722

redis 中

image-20210924005359101
(2)扣减信号量(停进来1辆车,车位-1)

扣减5次信号量后(停车5次)

image-20210924005527589

第6次,就一直加载,停不成功,因为没有信号量(车位)了

image-20210924005651126
(3)增加信号量(开出去1辆车,车位+1)

只有当开走一辆车后,才能再停进来一辆

image-20210924005949474

8、闭锁(CountDownLatch)

① 介绍

基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
② LockTestController
    //CountDownLatch 闭锁
    @GetMapping("/shenlong")
    public String shenlong() throws InterruptedException {
        RCountDownLatch zhsl = redissonClient.getCountDownLatch("zhsl");
        zhsl.trySetCount(7);  //需要七龙珠
        zhsl.await(); //等待龙珠集齐

        return "很大的神龙.....";
    }

    @GetMapping("/longzhu")
    public String longzhu(){
        RCountDownLatch zhsl = redissonClient.getCountDownLatch("zhsl");
        zhsl.countDown();
        return "收集了一颗龙珠";
    }
③ 测试

(1)神龙一直在等,等七颗龙珠集齐

image-20210924010831893

(2)收集了一颗龙珠

image-20210924010621098

redis 中的 zhsl(召唤神龙)

image-20210924010913376

连续刷新7次(收集七颗龙珠),召唤神龙

image-20210924011150946

9、分布式集合(映射(Map))

① 介绍

基于Redis的Redisson的分布式映射结构的RMap Java对象实现了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。与HashMap不同的是,RMap保持了元素的插入顺序。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

除了同步接口外,还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。如果你想用Redis Map来保存你的POJO的话,可以考虑使用分布式实时对象(Live Object)服务。

在特定的场景下,映射缓存(Map)上的高度频繁的读取操作,使网络通信都被视为瓶颈时,可以使用Redisson提供的带有本地缓存功能的映射。

RMap<String, SomeObject> map = redisson.getMap("anyMap");
SomeObject prevObject = map.put("123", new SomeObject());
SomeObject currentObject = map.putIfAbsent("323", new SomeObject());
SomeObject obj = map.remove("123");

map.fastPut("321", new SomeObject());
map.fastRemove("321");

RFuture<SomeObject> putAsyncFuture = map.putAsync("321");
RFuture<Void> fastPutAsyncFuture = map.fastPutAsync("321");

map.fastPutAsync("321", new SomeObject());
map.fastRemoveAsync("321");

映射的字段锁的用法:

RMap<MyKey, MyValue> map = redisson.getMap("anyMap");
MyKey k = new MyKey();
RLock keyLock = map.getLock(k);
keyLock.lock();
try {
   MyValue v = map.get(k);
   // 其他业务逻辑
} finally {
   keyLock.unlock();
}

RReadWriteLock rwLock = map.getReadWriteLock(k);
rwLock.readLock().lock();
try {
   MyValue v = map.get(k);
   // 其他业务逻辑
} finally {
   keyLock.readLock().unlock();
}
② LockTestController

本地集合的局限性是只能在本地存取,而分布式集合大家都可以存取

本地集合都是在内存中的数据,分布式集合是在 redis 中的数据

    /**
     * 以前用的 List,Map都在 内存中存的数据
     * <p>
     * 分布式集合。
     * 1)、接下创建的集合里面存的数据都是在redis里面所有人都能用
     */
    @GetMapping("/pua")
    public String distributeCollection() {
        // 分布式:名一样,就是同一个 map
        RMap<String, String> temp = redissonClient.getMap("temp");

        // 本地:对象一样,就是同一个 map
//        Map  map = new HashMap();
//        map.put()
//        map.put()

        String string = UUID.randomUUID().toString();
        temp.put("hello", string);
        return string;
    }

    @GetMapping("/gea")
    public String getDistributeCollection() {
        RMap<String, String> temp = redissonClient.getMap("temp");
        String hello = temp.get("hello");
        return hello;
    }
③ 测试

在 9000 中存的 map

image-20210924012000967

在 9002 中可以取到 9000 的 map

image-20210924012040370

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值