随着业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。说到分布式,不得不提的就是分布式锁和分布式事物。今天我们就来谈谈redis实现的分布式锁的问题!

实现要求:
1.互斥性,在同一时刻,只能有一个客户端持有锁
2.防止死锁,如果持有锁的客户端崩溃而且没有主动释放锁,怎样保证锁可以正常释放,使得客户端可以正常加锁
3.加锁和释放锁必须是同一个客户端。
4.容错性,只有redis还有节点存活,就可以正常的加锁解锁操作。
错误使用方式一:
保证互斥和防止死锁,首先想到的使用redis的setnx命令保证互斥,为了防止死锁,需要设置一个超时时间。

public Object getAndSet(String key, Object value, long timeout) {
        Object object = redisTemplate.opsForValue().getAndSet(key, value);
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        return object;
    }

在多线程并发环境下,任何非原子性的操作,都可能导致问题。在这段代码中,如果设置过期时间,redis实例崩溃,就无法设置过期时间。如果客户端没有正确释放锁,那么该锁永远不会过期,就永远不会被释放。

错误方式二
比较容易想到的就是设置值和超时时间为原子操作不就可以了吗。那么使用方法就是这样了

public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
        long expireTs = System.currentTimeMillis() + expireTime;
        // 锁不存在,当前线程加锁成果
        if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
            return true;
        }
        String value = jedis.get(key);
        //如果当前锁存在,且锁已过期
        if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
            //锁过期,设置新的过期时间
            String oldValue = jedis.getSet(key, String.valueOf(expireTs));
            if (oldValue != null && oldValue.equals(value)) {
                // 多线程并发下,只有一个线程会设置成功
                // 设置成功的这个线程,key的旧值一定和设置之前的key的值一致
                return true;
            }
        }
        // 其他情况,加锁失败
        return true;
    }

这段代码,乍一眼看没啥问题,你仔细看就会发现:
1.value 设置为过期时间,就要要求各个客户端严格的时钟同步,这需要使用到同步时钟。即使有同步时钟,分布式的服务器一般也会有少许误差,这不重要
2. 锁过期时,使用jedis.getSet虽然可以保证一个线程设置成功,但不能保证加锁和解锁为同一个客户端,因为没有标志时那个客户端设置的

解锁错误方式一:
直接删除key

public static void wrongReleaseLock(Jedis jedis, String key) {
        //不是自己加锁的key,也会被释放
        jedis.del(key);
    }

简单粗暴,但这样做的话,不是自己的锁也会被删除掉。不够严谨

解锁错误方式二:
判断自己是不是锁的持有者,只有持有者才可以释放锁

public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
        if (uniqueId.equals(jedis.get(key))) {
            // 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁
            jedis.del(key);
        }
    }

完美!
真的完美?
看起来很完美,但是如果你判断的时候锁是自己持有的,这时候超时自动释放了,然后又被其他客户端重新上锁了,然后你删除的不就是其他客户端的锁,一样不就乱套了?

基于以上信息探索,给出以下示例,仅供学习交流!
1.命令必须保证是互斥的
2. 设置的key必须要有过期时间
3. value使用唯一id,标志每个客户端。只有锁的持有者才能释放锁。
加锁直接使用set命令同时设置唯一id和过期时间;其中解锁些微复杂些,加锁后可以返回唯一ID,标志此锁是该客户端锁拥有;释放锁时要先判断是否是自己,只有自己才有删除操作,代码示例如下:

@Component
@Slf4j
public class RedisLockUtil {
    // 超时时间
    private static int EXPIRE_TIME = 5 * 1000;

    @Autowired
    private RedisTemplate redisTemplate;

    private static Map<String, Thread> threadMap = new ConcurrentHashMap();


    public Object lock(String key, Long timeOut) {
        log.info("加锁开始");
        try {
            // 超时等待时间
            Long waitEnd = System.currentTimeMillis() + EXPIRE_TIME;

            // 生成一个uuid,使得分布式调用有一个拥有者
            String uuid = UUID.randomUUID().toString();

            String value = key + uuid;
            // 在等待时间内,尝试获取锁
            while (System.currentTimeMillis() < waitEnd) {
                log.info("尝试获取锁");
                // 同步代码,使得操作原子性
                synchronized (this) {
                    if (Objects.nonNull(redisTemplate.opsForValue().get(key))) {
                        continue;
                    }
                    Object result = redisTemplate.opsForValue().getAndSet(key, value);
                    if (Objects.isNull(result)) {
                        log.info("成功获取锁");
                    }
                    // 设置过期时间,以防死锁
                    redisTemplate.expire(key, timeOut, TimeUnit.MILLISECONDS);

                    // 开启一个守护进程,给当前锁动态添加时间
                    Thread thread = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            while (true) {
                                try {
                                    if(System.currentTimeMillis() > waitEnd) {
                                        System.out.println(Thread.currentThread().getName() + "-->" + " 更新redis时间2s ");
                                        redisTemplate.expire(key, 1 * 60000, TimeUnit.MILLISECONDS);
                                        Thread.sleep(1000);
                                    }
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    });
                    thread.setDaemon(true); // 守护进程
                    threadMap.put(value, thread);
                    thread.setName(key+"-"+value);
                    thread.start();
                    return value;
                }
            }

        }catch (Exception e) {
            log.error("lock error:", e);
            throw new RuntimeException("未能获取分布式锁");
        }
        log.info("获取锁失败");
        throw new RuntimeException("获取分布式锁超时");
    }
    public boolean unLock(String key, Object value) {
        log.info("释放锁:{}--{}", key, value);
        if (Objects.isNull(key) ) {
            return false;
        }
        DefaultRedisScript script = new DefaultRedisScript();
        script.setResultType(List.class);
        script.setScriptText("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end");

        Object o = redisTemplate.execute(script, Collections.singletonList(key), value);
        if (Objects.nonNull(o) && ((ArrayList)o).size() !=0) {
            threadMap.remove(value).stop();
        }

        log.info("释放锁{}", o);

        return true;
    }
}

模拟调用代码

@GetMapping("/hello")
    public Object hello(String hello) {
        log.info("设置key值开始!");
       Object object = redisLockUtil.lock(REDIS_KEY, 1*60000L);
       try {
           log.info("设置key值{}", object);
           // 这里是模拟业务处理场景
           try {
               Thread.sleep(1 * 60000L);

           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       } catch (Exception e) {

       }finally {
           redisLockUtil.unLock(REDIS_KEY, object);
       }

        return object;
    }