Day07 缓存与分布式锁
一、答疑
1、如何开启 OpenFeign 的日志功能
loggin.level.你feign接口所在的包 : debug 就可以
例如:
![image-20210911162032362](https://i-blog.csdnimg.cn/blog_migrate/a374079a99f2306f3e08c9bb73c0fe6f.png)
2、缓存穿透问题
Redis的穿透,如果我用随机值去攻击的话,那么上课讲的设置 null 值的方法不就没有效果了吗?
- 每次 key 都是 uuid 的东西
- 每次缓存没有,去查数据库,然后返回 null,再缓存
- 既浪费了 redis,数据库还防止不住
- 缓存 null 值已经没有作用了
解决方法:布隆过滤器(下次用)【使用少量空间,来做到判断海量数据,允许误判】
![image-20210911155110577](https://i-blog.csdnimg.cn/blog_migrate/0e428dffdccf50309ecf6dad03f52572.png)
二、高并发下缓存失效问题
1、缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃,随机 key 大量攻击
解决:null 结果缓存,并加入短暂过期时间,进阶加上布隆过滤器前置
![image-20210911162808264](https://i-blog.csdnimg.cn/blog_migrate/0680ce79f52c58f0facc5ad644c66b86.png)
2、缓存击穿
- 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。 例如:item.jd.com/777.html
- 如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决:加锁
大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
![image-20210911163010653](https://i-blog.csdnimg.cn/blog_migrate/0ecab7656d74397fec9c78fae9b5eddc.png)
3、缓存雪崩
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻集体失效,请求全部转发到DB,DB瞬时压力过重雪崩。
各种数据都在缓存中有
1: 300
2: 800
3: 100 xxx
解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
![image-20210911163119858](https://i-blog.csdnimg.cn/blog_migrate/1bffab78538b4664bd9f8d2dd3ce66d8.png)
三、分布式下如何加锁
![image-20210911192502267](https://i-blog.csdnimg.cn/blog_migrate/0d290bc6a31902ccadc15a4317c64f9a.png)
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](https://i-blog.csdnimg.cn/blog_migrate/b1e8bea034d0ddef61021daf5a09065e.png)
2、分布式锁的原理演示
① 先把会话复制多份
![image-20210922123719277](https://i-blog.csdnimg.cn/blog_migrate/331c56bac7213d4310fdde123cea1458.png)
![image-20210922134231803](https://i-blog.csdnimg.cn/blog_migrate/ca6ca3e7d0420b06f8b27faf1bb90e44.png)
4个会话都进来了
![image-20210922134328276](https://i-blog.csdnimg.cn/blog_migrate/75d5a4a33d2d1c5285a98329067f5488.png)
1~3 号都没有蹲成功
![image-20210922134742680](https://i-blog.csdnimg.cn/blog_migrate/00c8eac42d6569ef98bb55b37ffdeb8a.png)
4号蹲成功了
![image-20210922134818014](https://i-blog.csdnimg.cn/blog_migrate/d25598685af7da5b3ed999640404463a.png)
如果为空就set值,并返回1
如果存在(不为空)不进行操作,并返回0
3、阶段一
![image-20210922220837575](https://i-blog.csdnimg.cn/blog_migrate/c46305de2407b0114c4cef2e42256fcc.png)
那么接来下我们就结合分布式锁的原理和阶段一完成分布式锁版的获取商品详情
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](https://i-blog.csdnimg.cn/blog_migrate/222c692bc012e05843a878c61b7d0562.png)
9000:缓存命中
![image-20210922210438758](https://i-blog.csdnimg.cn/blog_migrate/b00c72272b1ac7caedccdc720e7d641a.png)
9001:百万并发进来,它先抢占成功了,然后开始远程查询数据库
查询数据库成功以后,它就把锁释放了,因为它已经查询到了,其他人也就不用再查数据库了,直接查询缓存就有
9002:缓存命中
9003:缓存命中
因为 9001 干活太快了,以至于其他三个都没有抢锁的机会
⑦ 漏洞
这个版本有漏洞
![image-20210922220821253](https://i-blog.csdnimg.cn/blog_migrate/75ebd55a49c39154d48ed83acf48c17a.png)
![image-20210922220635599](https://i-blog.csdnimg.cn/blog_migrate/00e85bbea9e1e06bf799fa3e1cf91a7a.png)
那么,用 try-catch-finally,把删锁代码放到 finally 中可以防止这个问题吗?
try{
//业务代码 1000
}finally {
stringRedisTemplate.delete("lock");
}
答案是不可以!try-catch-finally 只能保证在正常情况下有用,但是如果是一些特殊情况下就没用了,比如在执行业务代码的时候突然断电了,代码根本都执行不到 finally 中,删锁代码还是没有执行
单靠 try-catch-finally 是不够的的,害得要加上 redis 的过期时间,这样就算在执行业务代码的时候突然断电了,finally中的删锁代码没执行,但是 redis 加了过期时间,它会自己删
这就是我们的阶段二
5、阶段二
![image-20210922225237221](https://i-blog.csdnimg.cn/blog_migrate/a9c3a7db839361c03c92ff1d3484f06c.png)
redis 设置过期时间
![image-20210922224300227](https://i-blog.csdnimg.cn/blog_migrate/bda2cf6f4b0c914b1bf9f8a045846ffc.png)
① 思考:这样就能保证万无一失了吗?
![image-20210922224514647](https://i-blog.csdnimg.cn/blog_migrate/6f5cc5aad1625d55f83d6ca5d689abf0.png)
还有可能出现这种问题,因为 redis 有两次操作,第一次是占坑,第二次是设置过期时间,中间会有空档期,万一这个空档期出问题了导致过期时间没设置上,那还是等于没设置,依然会出现锁无法释放,其他人永远抢不到锁
② 解决方法
给 redis 把话一次性说完,别分两次说。也即第一次占坑就设置过期时间,保证原子性
这就是我们的阶段三了
6、阶段三
![image-20210923002045997](https://i-blog.csdnimg.cn/blog_migrate/93b3e8ac97134ac7a0c16a5b9d9a3973.png)
① 解决阶段二的问题
redis 在底层有这个命令的支持
set lock 1 NX EX 20
# setnx(set if not exist)
# setex(set expire value)
![image-20210922225915144](https://i-blog.csdnimg.cn/blog_migrate/5c016dc395065cdfa39d018b938715ef.png)
// 底层调用 set lock 1 NX EX 20
Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lock", "aaa",20, TimeUnit.SECONDS);
![image-20210922232931668](https://i-blog.csdnimg.cn/blog_migrate/60217ba0d165deacb94daf3584f6fe09.png)
这样一次就指定好了 redis 占坑+设置过期时间了
② 漏洞
可能会出现超过锁的过期时间还未完成业务,此时另一个线程会侵入,因为锁的 key 是一样的,所以等第一个线程结束业务后会释放第二个线程的锁,但是这个 key 别人正在用,第二个线程的锁被释放后,第三个线程就进来了…这样就会导致至少有两个人都在运行这段代码,相当于我们的分布式锁锁了个寂寞
场景:我们设置锁的过期时间为10s,线程 A 的业务代码执行的时间就是长,用了15s才执行完,但是在第10s的时候锁的过期时间已经到了,这个锁已经被删了,但由于锁已经被删了马上就有线程B进来了,线程 B 执行到第5s的时候线程 A 已经执行完了,此时 A 就要执行删锁代码,因为用的是同一把锁,锁的 key 是一样的,因此 A 就把 B 的锁给删除了;此时 线程 C 抢到了锁进来了…
③ 解决方法
删除锁的时候用自己的值,以前我们只是占坑,根本不关心坑里面的东西(value 的值)是什么,现在我们可以整一个UUID,生成一个唯一字符串 token,然后在删锁之前将 token 和 value 进行比价,看两者是否相同,如果相同则说明是自己的锁,此时再删除;如果不相同则说明不是自己的锁,就不能删除
这个解决方法就是阶段四了
7、阶段四
![image-20210923093346234](https://i-blog.csdnimg.cn/blog_migrate/4d9f1e2a7372e061100e7e5e701d2fea.png)
![image-20210923114428822](https://i-blog.csdnimg.cn/blog_migrate/12a8c64e536282c93108f6113de9b898.png)
![image-20210923114517634](https://i-blog.csdnimg.cn/blog_migrate/f3ccedb459f7babcfefaa53ea30ab3e4.png)
① 思考:这样就能保证万无一失了吗?
![image-20210923124948182](https://i-blog.csdnimg.cn/blog_migrate/951bee6bba657826ba84f0a2f15151e0.png)
归根结底还是因为获取值和删除锁是两步的,没有保证原子性
![image-20210923121016603](https://i-blog.csdnimg.cn/blog_migrate/b036ca8a2e864f70db5a79b5479c194d.png)
要把这两步变成一步,保证原子性
② 解决方法
利用脚本把两步变成一步,这就是阶段五了
8、阶段五(最终形态?1)
![image-20210923121510889](https://i-blog.csdnimg.cn/blog_migrate/7141d45416cf6f64800767a6c0b761c3.png)
① 解决阶段四的问题
![image-20210923121833607](https://i-blog.csdnimg.cn/blog_migrate/1d8723a79ed06f86f92742077f7fd93e.png)
// 告诉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](https://i-blog.csdnimg.cn/blog_migrate/04fe10a33a86f0d64c4a1cf8b1faeaf6.png)
线程池热完身 redis 里面就有数据了,我们此时再把 redis 里面的数据 flush 掉
![image-20210923150059158](https://i-blog.csdnimg.cn/blog_migrate/607d5d88700efbec1158af6dcd0d21ed.png)
然后重新来,把控制台的信息清空
我们自己单元测试,debug 方式启动,手动模拟把 lock 放进 redis
![image-20210923150525517](https://i-blog.csdnimg.cn/blog_migrate/b6b41f7ab513228fd8d11d6defc75df1.png)
前端发请求
![image-20210923150655490](https://i-blog.csdnimg.cn/blog_migrate/9ad42392d11d069e9cda646f64417615.png)
然后看后端,9000 因为没有缓存命中,开始抢锁,没抢成功正在不断自旋中
![image-20210923150729123](https://i-blog.csdnimg.cn/blog_migrate/3abd9467d43cf6de683ac56a08bc4977.png)
我们手动把 redis 中的 lock 删除,模拟让 9000 自旋占锁成功
![image-20210923151121612](https://i-blog.csdnimg.cn/blog_migrate/ad4693872cf2bc8109ce68c0e5d02d43.png)
自旋占锁成功,又调自己,我们 step into 进去
![image-20210923151325661](https://i-blog.csdnimg.cn/blog_migrate/a1c8b77a56d27263c3e2e2a61a057782.png)
缓存中没有,因为我们占锁成功还没人查
![image-20210923151445683](https://i-blog.csdnimg.cn/blog_migrate/68be0281d807ce50b83967eeb6ea3105.png)
开始抢锁查数据库,这个锁就是我自己的,所以直接就是 true,占锁成功
![image-20210923151545177](https://i-blog.csdnimg.cn/blog_migrate/f58a042a0c5db2383d67ff1c5a5ebbe2.png)
![image-20210923151849906](https://i-blog.csdnimg.cn/blog_migrate/9f11edd9ba2fe5733308528ee04e5a18.png)
② 有个 bug
这里自旋成功后,进入 getSkuInfoWithRedisLock01 方法,
![image-20210923152918322](https://i-blog.csdnimg.cn/blog_migrate/f65a8cb6c7c6f29482ffdf2b4d68bf70.png)
但是这个方法里面又会判断是否缓存命中,如果不命中则又要开始抢锁
![image-20210923153415591](https://i-blog.csdnimg.cn/blog_migrate/48c4ddd35970432c7d651429f88a27e9.png)
所以相当于抢了两遍,我们刚刚 debug 测试能成功,是因为在自旋的时候抢锁,但是在 cache = getSkuInfoWithRedisLock01(skuId) 时已经过了 10s 超时了,redis 删了
![image-20210923153847690](https://i-blog.csdnimg.cn/blog_migrate/1d6c0c65b95e91527f81371948dcbdb6.png)
如果我们在这里没有超时时间,这个锁是可重入的吗?
![image-20210923154223269](https://i-blog.csdnimg.cn/blog_migrate/48607ef54506dd23bdea4fe1fff3e023.png)
如果设计为可重入锁
![image-20210923154346077](https://i-blog.csdnimg.cn/blog_migrate/0b46d020a6eeafd4a8b94f90dedb4439.png)
![image-20210923154428524](https://i-blog.csdnimg.cn/blog_migrate/0bb0b1e1fb987c43c8113399706ad320.png)
就不用抢了
我们希望这个锁能往下传递
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](https://i-blog.csdnimg.cn/blog_migrate/9f29e8f9bbdef5488138707ec690ba4b.png)
① 测试 + 分析
先把 redis 中的数据删掉
![image-20210923155734914](https://i-blog.csdnimg.cn/blog_migrate/63810a011280957101a6dbabfa2a3aa9.png)
为了让它们抢占锁,我们手动在 redis 中加一个锁
![image-20210923155825849](https://i-blog.csdnimg.cn/blog_migrate/4a99d1059f5a49dfafc0cd10f1d806d9.png)
什么叫可重入锁?
// 伪代码
A {
// A 调用 B 加了锁
B {
// B 方法说我也要加这把锁
}
}
如果设计为不可重入的:A 因为有了这个锁,B 想要加 A 这把锁,它就一直加不上,必须等 A 释放,但是 A 怎么释放呢?A 还等着 B 执行完才能释放,形成两个方法都要用同一把锁,所以锁必须到设计为可重入的
所谓可重入锁,就是 A 的锁,B 可以直接拿过来用,B 看 A 占了就直接拿过来用,反正 B 是自旋过来的,肯定要直接用
进来先查缓存中有没有,发现缓存中没有
![image-20210923161032901](https://i-blog.csdnimg.cn/blog_migrate/a0847ed030e88fee4d7c20e59dc9517a.png)
缓存不命中,开始抢锁
![image-20210923161122386](https://i-blog.csdnimg.cn/blog_migrate/124af95d871844707ec4fca534fcf321.png)
它没抢到锁,因为我们在上面通过手动在 redis 中添加了锁,这里肯定是抢不到的
![image-20210923161211319](https://i-blog.csdnimg.cn/blog_migrate/2574ef57e6dec0e0c7fc5cc1385314c7.png)
抢不到开始自旋抢锁
![image-20210923161346502](https://i-blog.csdnimg.cn/blog_migrate/70337b7578b3d0cdb4f706621c73e04c.png)
假设这次自旋抢到了(我们手动把 redis 中的 lock 删除)
![image-20210923161448300](https://i-blog.csdnimg.cn/blog_migrate/850ea3329f2883a91a21a71fd20786ad.png)
抢到了,锁是 0afe3a5b-a68b-XXXX
![image-20210923161520194](https://i-blog.csdnimg.cn/blog_migrate/a98d6b574a3d8bd5e8e764bcc5b1e88f.png)
![image-20210923161639361](https://i-blog.csdnimg.cn/blog_migrate/f98e43b3c4b6d1481eafe6a5a35626c4.png)
抢成功了,继续执行业务逻辑,step into 进来看锁能不能可重入
![image-20210923161725313](https://i-blog.csdnimg.cn/blog_migrate/bb41d9dcc2edeaac28d8526dac2b1d2c.png)
先查缓存,缓存中没有
![image-20210923173823839](https://i-blog.csdnimg.cn/blog_migrate/12a5711b2b8374266cc45a1eac069ee9.png)
开始抢锁,又生成了一个 token
![image-20210923173920613](https://i-blog.csdnimg.cn/blog_migrate/33beb5cc5efc0a799aca318eef31fd52.png)
这个新生成的 token 与 redis 中的 lock 的值不相同
![image-20210923174030300](https://i-blog.csdnimg.cn/blog_migrate/b823ae8f7ef327e61aca6492320a1965.png)
不相同,肯定是占锁失败,而且我明确的告诉你会一直失败,因为上一个人进来这个锁已经帮你占了,锁重入直接进来就行了
![image-20210923174144391](https://i-blog.csdnimg.cn/blog_migrate/2eb62dcfa554ab7165d0fce5c10b00e3.png)
现在就是这么个情况
自旋尝试占锁了,而且都已经帮你占好锁了
![image-20210923174855073](https://i-blog.csdnimg.cn/blog_migrate/a0afaaf3ec019dc8e8912f0ba7340b6d.png)
然后进入方法里面,你直接用就可以了
![image-20210923174919160](https://i-blog.csdnimg.cn/blog_migrate/76badf5165331c37d5a69ae1bfaac4d2.png)
但是现在发现这块用不到,还要抢锁,但是 token 与 redis 中的有不同,所以一致抢锁失败
![image-20210923174955862](https://i-blog.csdnimg.cn/blog_migrate/18e8eb4643ea8fa499c54d9329594168.png)
我们现在的锁唯一的问题就是非重入的,上面抢占失败就得自旋,自旋继续占锁,占锁失败又得自旋…,其实在第一次自旋的时候都已经帮你占好锁了,占好以后我希望你以后直接使用
也就是同一线程里面锁应该直接使用
我们把 getSkuInfoWithRedisLock01 改成可重入锁版
11、getSkuInfoWithRedisLock01(可重入锁版)
① 改进代码
在 getSkuInfoWithRedisLock01() 中再加两个参数:Boolean locked :是否锁定了,String beforeToken 如果锁定了它用的 token 是什么
![image-20210923175819664](https://i-blog.csdnimg.cn/blog_migrate/aeb54d90c361695c3e6965a0f2b93e52.png)
第一次调用 getSkuInfoWithRedisLock01 时没有锁定,token 是 null
![image-20210923180137858](https://i-blog.csdnimg.cn/blog_migrate/cb7aa8f44977eb808cafb6891e5d5ab3.png)
当自旋,占锁成功以后,就把占锁成功的 locked 和 token 往下传
![image-20210923180420069](https://i-blog.csdnimg.cn/blog_migrate/7b3a8b9b9b8fb1988637d059b6abcbed.png)
@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](https://i-blog.csdnimg.cn/blog_migrate/cf0427996e7a327043a393e3cc7ac986.png)
第一次进来 locked 是 false,token 是 null
![image-20210923182022268](https://i-blog.csdnimg.cn/blog_migrate/aa2b33141a0eaaf1d6a18fe27907a1e5.png)
先查缓存中有没有,缓存中没有
![image-20210923182118186](https://i-blog.csdnimg.cn/blog_migrate/dedba2827c6d90734cc9b5f08417682d.png)
缓存中没有准备加锁查,先判断别人有没有已经帮我锁定的,发现别人没有帮我锁定 locked 是 false
![image-20210923182313028](https://i-blog.csdnimg.cn/blog_migrate/a38d45beaa219625fc2e833f997b87ce.png)
所以进来自己加锁,自己加的锁 token 是 bc20ceea-d4cf-47a8-783cacfe7e3f
![image-20210923182639534](https://i-blog.csdnimg.cn/blog_migrate/6f7f1bab806c9d6927f91ad99681156a.png)
redis 中的 lock 值
![image-20210923182830285](https://i-blog.csdnimg.cn/blog_migrate/86b1c6e78973ed5e7f59453bd750af1b.png)
这个锁加不成功,因为 redis 中已经有了
![image-20210923182914453](https://i-blog.csdnimg.cn/blog_migrate/8c8d9b9ba85b82bda366a7a81705f8a5.png)
加锁失败开始自旋,不断抢
![image-20210923182956749](https://i-blog.csdnimg.cn/blog_migrate/1b74e8d644795576734d3a9df766d526.png)
当我们将 redis 中的 lock 手动删除后
![image-20210923183057763](https://i-blog.csdnimg.cn/blog_migrate/f73f6b4c1a683080a13f80a10f9f2a70.png)
生成了一个新的 token2 ,自旋占锁成功
![image-20210923183219278](https://i-blog.csdnimg.cn/blog_migrate/b72fbd02eed61b70859f2b55920d8b72.png)
此时再看 redis
![image-20210923183321274](https://i-blog.csdnimg.cn/blog_migrate/95d6f5ec3f09baa42c859f2ab77f9a7c.png)
继续往下执行,要调用 getSkuInfoWithRedisLock01 方法了,会把自旋占锁的结果(true,fbed2XXX)传过来
![image-20210923183449133](https://i-blog.csdnimg.cn/blog_migrate/7f39ae196da358a4fc9cc34296ce2803.png)
step into 进入 getSkuInfoWithRedisLock01 方法,进来先查缓存中有没有,缓存中没有
![image-20210923183859008](https://i-blog.csdnimg.cn/blog_migrate/421b0f03c76645580ff34d1a9dd7ffd7.png)
因为前人已经帮我占好锁了(前人栽树后人乘凉),我把前人的 token(fbedXXX) 和 locked(true)拿来,然后判断 locked
![image-20210923184039977](https://i-blog.csdnimg.cn/blog_migrate/218343a29057d43bc67fe28e1bc70186.png)
前人已经帮我占好锁了,我都不用锁,直接就抢占成功了
![image-20210923184438007](https://i-blog.csdnimg.cn/blog_migrate/b7500c522a0eb997c04e436b50376a9e.png)
执行业务代码:查询数据库,将数据放入缓存
![image-20210923184734821](https://i-blog.csdnimg.cn/blog_migrate/cd6286b6c033f9b47bb75ee5e70ae4a7.png)
业务代码执行完后准备删锁
![image-20210923184922255](https://i-blog.csdnimg.cn/blog_migrate/c7a3e9e3cd83d9dcb83da9fecd06399d.png)
因为是上一次重入的人给我的锁,所以我删的是 fbedXXXXX,没毛病
![image-20210923184955770](https://i-blog.csdnimg.cn/blog_migrate/502f00c42b1ed1e36b85abfe98e09c5a.png)
至此,所有的逻辑已经走完
⑤ 小总结
不可重入锁会死锁,未来设计的所有锁都应该是可重入锁
我们写个可重入锁写得这么难受,那有没有已经封装好的框架呢?唉,于是乎我们的 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](https://i-blog.csdnimg.cn/blog_migrate/acbd3d7914751d34d7de0c937adf1095.png)
新建 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](https://i-blog.csdnimg.cn/blog_migrate/1acab210b5f972c2ade2592b8fd8eee3.png)
3、可重入锁-lock()
![image-20210923211802121](https://i-blog.csdnimg.cn/blog_migrate/1c2a24c8fd6bf347c19451add20ee117.png)
// 只要 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](https://i-blog.csdnimg.cn/blog_migrate/1dbb2491c4228dffe890e7d3e82bbdd2.png)
![image-20210923223904760](https://i-blog.csdnimg.cn/blog_migrate/ca04018302196e93dcb4d3a27c5a448b.png)
添加监听器—汇总报告
![image-20210923223940168](https://i-blog.csdnimg.cn/blog_migrate/8941cb190225e8d10df853516b113b13.png)
添加监听器—聚合报告
![image-20210923224025183](https://i-blog.csdnimg.cn/blog_migrate/03477c72f9013fe2ef473e89019204a0.png)
线程组添加取样器—HTTP请求
![image-20210923224123054](https://i-blog.csdnimg.cn/blog_migrate/dc02fe50620d4825aaf1848de5778ed4.png)
启动
![image-20210923224339162](https://i-blog.csdnimg.cn/blog_migrate/cb353a827f2b4c5a04bbf0002f00916f.png)
查看汇总报告
查看聚合报告
如果要重新测试的话,得先清除报告
③ Console 结果
9000:
![image-20210923224647206](https://i-blog.csdnimg.cn/blog_migrate/29e5f6ede7ba18926a75057f87f894da.png)
![image-20210923224744570](https://i-blog.csdnimg.cn/blog_migrate/61a479c4215af39e549f75aa6a6ebdfd.png)
9001:只有9001查询了一遍数据库,注意线程号,线程号相等的是一组
![image-20210923225321169](https://i-blog.csdnimg.cn/blog_migrate/a7726c74493361919f1bac9d77b27958.png)
为什么打印这么多的加锁成功...尝试继续命中缓存
?因为上一个人加锁成功后尝试查缓存,缓存中有就直接返回,这个时候就把锁给释放了,下一个人就加锁成功了
ab 测试不准,以后压力测试就用 jmeter
4、redisson 使用小结 & 面试题
① redisson 会死锁吗?
答:不会,因为 redisson 是可重入锁(同一线程内前面加过后面不用加)
(1)redisson 会自动解锁吗?
如果 finally 中的解锁代码 lock.unlock();
没有调到,比如说突然断电了,redisson 会自动解锁吗?
![image-20210923230553945](https://i-blog.csdnimg.cn/blog_migrate/efc82bf41778164fcacae49df2b4f25d.png)
答案是:会
看门狗30s,代码闪断,定时(守护)线程没有了,就不自动续期了,等30s,redis自己删除,默认加锁就是30s
(2)推荐不要使用 lock(30,TimeUnit.SECONDS);
因为如果这样设置了,就会失去了自动续期功能
scheduleExpirationRenewal(threadId); 的调用前提是 if (leaseTime == -1) 没有续期功能;
lock() 源代码:
![image-20210923231725544](https://i-blog.csdnimg.cn/blog_migrate/648d0d28a2bb6bb428ebdeb434d52959.png)
![image-20210923232340667](https://i-blog.csdnimg.cn/blog_migrate/2ddf4f23ab7e9fb6a3a63dbad51fefd1.png)
![image-20210923232450726](https://i-blog.csdnimg.cn/blog_migrate/fab9d517cc85078f4db15d5bd2bd6efa.png)
![image-20210923232710582](https://i-blog.csdnimg.cn/blog_migrate/48b16af3b7c98196a67e39b30a69400c.png)
![image-20210923233317797](https://i-blog.csdnimg.cn/blog_migrate/21126e415353de602d1b1c4bfe334846.png)
发现设置了过期时间后就会自动续期了
(3)看门狗
看门狗只是为了当业务超时,锁时间不够来不停续期的
看门狗还有默认的30秒(lock.lock())自动过期(redis删),就是防止特殊情况
lock.lock() 的看门狗只是看当前线程,默认过期时间是30s,当前线程哪怕执行一秒就结束了,我们这看门狗也就结束了;当前线程一直在,看门狗就一直在
② redisson 分布式业务超时怎么办?
答:看门狗自动续期,每隔1/3看门狗时间就续满看门狗时间
5、可重入锁-trylock()
![image-20210924001332800](https://i-blog.csdnimg.cn/blog_migrate/273a7941c97dc260aac66c40a4662c26.png)
Redisson同时还为分布式锁提供了异步执行的相关方法:
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
① 同步加锁
![image-20210924002128253](https://i-blog.csdnimg.cn/blog_migrate/f3913502436d487dfd6baf7dd4ebf537.png)
② 异步加锁
![image-20210924002750016](https://i-blog.csdnimg.cn/blog_migrate/751cdd67841f0f8c11c7d925e5b326d5.png)
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](https://i-blog.csdnimg.cn/blog_migrate/947d98a014874023746570214ba9bc34.png)
(1)并发写
前端发请求
等第一个人写锁释放后,第二个人才能获得写锁
过了几秒后
(2)并发读
并发读是无锁的
(3)一写一读
写的时候还想要读,读就被阻塞了
只有写锁释放了,才能读
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](https://i-blog.csdnimg.cn/blog_migrate/ad91a3b95c409560e0bb27e92330b80c.png)
redis 中
![image-20210924005359101](https://i-blog.csdnimg.cn/blog_migrate/4eff264b4bb770b7f2a1a3ab3d994c19.png)
(2)扣减信号量(停进来1辆车,车位-1)
扣减5次信号量后(停车5次)
![image-20210924005527589](https://i-blog.csdnimg.cn/blog_migrate/40b7ae6acd568c679e2a16d84045bbfc.png)
第6次,就一直加载,停不成功,因为没有信号量(车位)了
![image-20210924005651126](https://i-blog.csdnimg.cn/blog_migrate/15558b445404b2d1ac95ff22f8ea0eda.png)
(3)增加信号量(开出去1辆车,车位+1)
只有当开走一辆车后,才能再停进来一辆
![image-20210924005949474](https://i-blog.csdnimg.cn/blog_migrate/d929e406e89ce7dd263eeab9feeb8a5c.png)
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](https://i-blog.csdnimg.cn/blog_migrate/bca2dd052d6f608942e28d0bfacd6694.png)
(2)收集了一颗龙珠
![image-20210924010621098](https://i-blog.csdnimg.cn/blog_migrate/f10ece9f931b2d262062412d26330826.png)
redis 中的 zhsl(召唤神龙)
![image-20210924010913376](https://i-blog.csdnimg.cn/blog_migrate/4ff741a6b75737446b2de232fd7ff866.png)
连续刷新7次(收集七颗龙珠),召唤神龙
![image-20210924011150946](https://i-blog.csdnimg.cn/blog_migrate/d9e4d357f2937f23cb5117efd1d3850d.png)
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](https://i-blog.csdnimg.cn/blog_migrate/4cd845e0b9c8cd1bc139e4fba97c098f.png)
在 9002 中可以取到 9000 的 map