谷粒商城-高级-48 缓存-分布式锁原理及使用
一、本地锁
锁-时序问题:
二、分布式锁演进
1、基本原理
我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。占坑可以去Redis,也可以去数据库,可以去任何大家都可以访问到的地方。等待可以自旋的方式。
2、阶段一
Redis官网NX参数说明
NX – 只有键key不存在的时候才会设置key的值。
解读:每个线程在访问时,先会去判断这个“坑位”有没有加锁,如果没有加锁,则自己占住加锁(然后获取数据,从缓存/数据库),别人来的时候看到加锁就会等待,当从坑位离开时,会删除锁,然后其他的才会继续站位。
进入redis:
> docker exec -it redis redis-cli
# 可以多开几个窗口进行测试
127.0.0.1:6379> set lock 1 NX
OK
3、阶段二
4、阶段三
5、阶段四
6、最终形态
分布式锁代码
gulimall/product/service/impl/CategoryServiceImpl.java
/**
* 从数据库查询并封装数据::分布式锁
* @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(); //自旋的方式
}
}
private Map<String, List<Catelog2Vo>> getDataFromDb() {
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
if (!StringUtils.isEmpty(catalogJson)) {
//缓存不为空直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库");
/**
* 将数据库的多次查询变为一次
*/
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;
}));
//3、将查到的数据放入缓存,将对象转为json
String valueJson = JSON.toJSONString(parentCid);
stringRedisTemplate.opsForValue().set("catalogJson", valueJson, 1, TimeUnit.DAYS);
return parentCid;
}
为者常成,行者常至
自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)