定制直播软件,分布式锁的演进你了解多少?
基本原理
我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。“占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。等待可以自旋的方式。
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {//阶段一Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");//获取到锁,执行业务if (lock) {Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();//删除锁,如果在此之前报错或宕机会造成死锁stringRedisTemplate.delete("lock");return categoriesDb;}else {//没获取到锁,等待100ms重试try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return getCatalogJsonDbWithRedisLock();}}public Map<String, List<Catalog2Vo>> getCategoryMap() {ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();String catalogJson = ops.get("catalogJson");if (StringUtils.isEmpty(catalogJson)) {System.out.println("缓存不命中,准备查询数据库。。。");Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb();String toJSONString = JSON.toJSONString(categoriesDb);ops.set("catalogJson", toJSONString);return categoriesDb;}System.out.println("缓存命中。。。。");Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});return listMap;}
问题: setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
解决: 设置锁的自动过期,即使没有删除,会自动删除
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");if (lock) {//设置过期时间stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS);Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();stringRedisTemplate.delete("lock");return categoriesDb;}else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return getCatalogJsonDbWithRedisLock();}}
问题: setnx设置好,正要去设置过期时间,宕机。又死锁了。
解决: 设置过期时间和占位必须是原子的。redis支持使用setnx ex命令。
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {//加锁的同时设置过期时间,二者是原子性操作Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS);if (lock) {Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();//模拟超长的业务执行时间try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}stringRedisTemplate.delete("lock");return categoriesDb;}else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return getCatalogJsonDbWithRedisLock();} }
问题: 删除锁直接删除???如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决: 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {String uuid = UUID.randomUUID().toString();ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();//为当前锁设置唯一的uuid,只有当uuid相同时才会进行删除锁的操作Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);if (lock) {Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();String lockValue = ops.get("lock");if (lockValue.equals(uuid)) {try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}stringRedisTemplate.delete("lock");}return categoriesDb;}else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return getCatalogJsonDbWithRedisLock();}}
问题: 如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁
解决: 删除锁必须保证原子性。使用redis+Lua脚本完成
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {String uuid = UUID.randomUUID().toString();ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS);if (lock) {Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap();String lockValue = ops.get("lock");String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +" return redis.call(\"del\",KEYS[1])\n" +"else\n" +" return 0\n" +"end";stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), lockValue);return categoriesDb;}else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}return getCatalogJsonDbWithRedisLock();}}
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期。
以上就是定制直播软件,分布式锁的演进你了解多少?, 更多内容欢迎关注之后的文章