首先是redis锁的工具类,包含了加锁和解锁的基础方法:
public class RedisLockUtil { private String LOCK_KEY = "redis_lock"; // key的持有时间,5ms private long EXPIRE_TIME = 5; // 等待超时时间,1s private long TIME_OUT = 1000; // redis命令参数,相当于nx和px的命令合集 private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME); // redis连接池,连的是本地的redis客户端 JedisPool jedisPool = new JedisPool("127.0.0.1", 6379); /** * 加锁 * * @param id * 线程的id,或者其他可识别当前线程且不重复的字段 * @return */ public boolean lock(String id) { Long start = System.currentTimeMillis(); Jedis jedis = jedisPool.getResource(); try { for (;;) { // SET命令返回OK ,则证明获取锁成功 String lock = jedis.set(LOCK_KEY, id, params); if ("OK".equals(lock)) { return true; } // 否则循环等待,在TIME_OUT时间内仍未获取到锁,则获取失败 long l = System.currentTimeMillis() - start; if (l >= TIME_OUT) { return false; } try { // 休眠一会,不然反复执行循环会一直失败 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } finally { jedis.close(); } } /** * 解锁 * * @param id * 线程的id,或者其他可识别当前线程且不重复的字段 * @return */ public boolean unlock(String id) { Jedis jedis = jedisPool.getResource(); // 删除key的lua脚本 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then" + " return redis.call('del',KEYS[1]) " + "else" + " return 0 " + "end"; try { String result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString(); return "1".equals(result); } finally { jedis.close(); } } }具体的代码作用注释已经写得很清楚了,然后我们就可以写一个demo类来测试一下效果:
public class RedisLockTest { private static RedisLockUtil demo = new RedisLockUtil(); private static Integer NUM = 101; public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { String id = Thread.currentThread().getId() + ""; boolean isLock = demo.lock(id); try { // 拿到锁的话,就对共享参数减一 if (isLock) { NUM--; System.out.println(NUM); } } finally { // 释放锁一定要注意放在finally demo.unlock(id); } }).start(); } } }我们创建100个线程来模拟并发的情况,执行后的结果是这样的:
可以看出,锁的效果达到了,线程安全是可以保证的。
当然,上面的代码只是简单的实现了效果,功能肯定是不完整的,一个健全的分布式锁要考虑的方面还有很多,实际设计起来不是那么容易的。
我们的目的只是为了学习和了解原理,手写一个工业级的分布式锁工具不现实,也没必要,类似的开源工具一大堆(Redisson),原理都差不多,而且早已经过业界同行的检验,直接拿来用就行。
虽然功能是实现了,但其实从设计上来说,这样的分布式锁存在着很大的缺陷,这也是本篇文章想重点探讨的内容,那到底存在哪些缺陷呢?
分布式锁的缺陷一、客户端长时间阻塞导致锁失效问题
客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题。
那么该如何防止这样的异常呢?我们先不说解决方案,介绍完其他的缺陷后再来讨论。
二、redis服务器时钟漂移问题
如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
三、单点实例安全问题
如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master加一个slave,但是因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,客户端1设置的锁丢失了,这时候客户端2设置锁也能够成功,导致客户端1和客户端2同时拥有锁。
为了解决Redis单点问题,redis的作者提出了RedLock算法。
RedLock算法该算法的实现前提在于Redis必须是多节点部署的,可以有效防止单点故障,具体的实现思路是这样的:
1、获取当前时间戳(ms);