目录:

(1)使用redis实现分布式锁

(2)优化之设置锁的过期时间

(3.)优化之UUID防误删

(4)优化之LUA脚本保证删除的原子性

(5) 总结


(1)使用redis实现分布式锁

(解决集群服务设置Reids的问题) 

1. 多个客户端同时获取锁(setnx)

2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)

3. 其他客户端等待重试

更改前面的代码

@Override
public void testLock() {
   // 1. 从redis中获取锁,setnx
   Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");
   if (lock) {
      // 查询redis中的num值
      String value = (String)this.redisTemplate.opsForValue().get("num");
      // 没有该值return
      if (StringUtils.isBlank(value)){
         return ;
      }
      // 有值就转成成int
      int num = Integer.parseInt(value);
      // 把redis中的num值+1
      this.redisTemplate.opsForValue().set("num", String.valueOf(++num));

      // 2. 释放锁 del
      this.redisTemplate.delete("lock");
   } else {
      // 3. 每隔1秒钟回调一次,再次尝试获取锁
      try {
         Thread.sleep(100);
         testLock();
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
}

重启,服务集群,通过网关压力测试:

基本实现。

问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放

解决:设置过期时间,自动释放锁。

(2)优化之设置锁的过期时间

setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放

设置过期时间,自动释放锁

设置过期时间有两种方式:

1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)

2. 在set时指定过期时间(推荐)

设置过期时间:

压力测试肯定也没有问题。自行测试

问题:可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是7s。执行流程如下

  1. index1业务逻辑没执行完,3秒后锁被自动释放。
  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
  3. index3获取到锁,执行业务逻辑
  4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,      导致index3的业务只执行1s就被别人释放。

最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁  

(3.)优化之UUID防误删

问题:删除操作缺乏原子性。

场景:

  1. 1  index1执行删除时,查询到的lock值确实和uuid相等

  1. 2.  index1执行删除前,lock刚好过期时间已到,被redis自动释放

在redis中没有了锁。

  1. 3  index2获取了lock,index2线程获取到了cpu的资源,开始执行方法

      1.4  index1执行删除,此时会把index2的lock删除

index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行

删除的index2的锁!

(4)优化之LUA脚本保证删除的原子性

@Override
public void testLock() {
    //  设置uuId
    String uuid = UUID.randomUUID().toString();
    //  缓存的lock 对应的值 ,应该是index2 的uuid
    Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock", uuid,1, TimeUnit.SECONDS);
    //  判断flag index=1
    if (flag){
        //  说明上锁成功! 执行业务逻辑
        String value = redisTemplate.opsForValue().get("num");
        //  判断
        if(StringUtils.isEmpty(value)){
            return;
        }
        //  进行数据转换
        int num = Integer.parseInt(value);
        //  放入缓存
        redisTemplate.opsForValue().set("num",String.valueOf(++num));

        //  定义一个lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        //  准备执行lua 脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //  将lua脚本放入DefaultRedisScript 对象中
        redisScript.setScriptText(script);
        //  设置DefaultRedisScript 这个对象的泛型
        redisScript.setResultType(Long.class);
        //  执行删除
        redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

    }else {
        //  没有获取到锁!
        try {
            Thread.sleep(1000);
            //  睡醒了之后,重试
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}    

Lua 脚本详解:http://doc.redisfans.com/string/set.html

(5) 总结

1、加锁

// 1. 从redis中获取锁,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
      .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);

2.使用lua释放锁 

// 2. 释放锁 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 设置lua脚本返回的数据类型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 设置lua脚本返回类型为Long
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

3.重试 

Thread.sleep(500);
testLock();

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

- 互斥性。在任意时刻,只有一个客户端能持有锁。

- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

- 加锁和解锁必须具有原子性

redis集群状态下的问题:

1. 客户端A从master获取到锁

2. 在master将锁同步到slave之前,master宕掉了。

3. slave节点被晋级为master节点

4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。

安全失效!

解决方案:了解即可!

更多推荐