这里使用了一个 lua
脚本来判断 value 是否相等,相等才执行 del 命令。
使用 lua
也可以保证这里两个操作的原子性。
因此上文提到的四个基本特性也能满足了:
- 使用 Redis 可以保证性能。
- 阻塞锁与非阻塞锁见上文。
- 利用超时机制解决了死锁。
- Redis 支持集群部署提高了可用性。
使用
我自己有撸了一个完整的实现,并且已经用于了生产,有兴趣的朋友可以开箱使用:
maven 依赖:
<dependency> <groupId>top.crossoverjie.opensource</groupId> <artifactId>distributed-redis-lock</artifactId> <version>1.0.0</version> </dependency>
配置 bean :
@Configuration public class RedisLockConfig { @Bean public RedisLock build(){ RedisLock redisLock = new RedisLock() ; HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ; JedisCluster jedisCluster = new JedisCluster(hostAndPort) ; // Jedis 或 JedisCluster 都可以 redisLock.setJedisCluster(jedisCluster) ; return redisLock ; } }
使用:
@Autowired private RedisLock redisLock ; public void use() { String key = "key"; String request = UUID.randomUUID().toString(); try { boolean locktest = redisLock.tryLock(key, request); if (!locktest) { System.out.println("locked error"); return; } //do something } finally { redisLock.unlock(key,request) ; } }
使用很简单。这里主要是想利用 Spring 来帮我们管理 RedisLock 这个单例的 bean,所以在释放锁的时候需要手动(因为整个上下文只有一个 RedisLock 实例)的传入 key 以及 request(api 看起来不是特别优雅)。
也可以在每次使用锁的时候 new 一个 RedisLock 传入 key 以及 request,这样倒是在解锁时很方便。但是需要自行管理 RedisLock 的实例。各有优劣吧。
项目源码在:
欢迎讨论。
单测
在做这个项目的时候让我不得不想提一下单测。
因为这个应用是强依赖于第三方组件的(Redis),但是在单测中我们需要排除掉这种依赖。比如其他伙伴 fork 了该项目想在本地跑一遍单测,结果运行不起来:
- 有可能是 Redis 的 ip、端口和单测里的不一致。
- Redis 自身可能也有问题。
- 也有可能是该同学的环境中并没有 Redis。
所以最好是要把这些外部不稳定的因素排除掉,单测只测我们写好的代码。
于是就可以引入单测利器 Mock
了。
它的想法很简答,就是要把你所依赖的外部资源统统屏蔽掉。如:数据库、外部接口、外部文件等等。
使用方式也挺简单,可以参考该项目的单测:
@Test public void tryLock() throws Exception { String key = "test"; String request = UUID.randomUUID().toString(); Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong())).thenReturn("OK"); boolean locktest = redisLock.tryLock(key, request); System.out.println("locktest=" + locktest); Assert.assertTrue(locktest); //check Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong()); }
这里只是简单演示下,可以的话下次仔细分析分析。
它的原理其实也挺简单,debug 的话可以很直接的看出来:
这里我们所依赖的 JedisCluster 其实是一个 cglib 代理对象
。所以也不难想到它是如何工作的。
比如这里我们需要用到 JedisCluster 的 set 函数并需要它的返回值。
Mock 就将该对象代理了,并在实际执行 set 方法后给你返回了一个你自定义的值。
这样我们就可以随心所欲的测试了,完全把外部依赖所屏蔽了。
总结
至此一个基于 Redis 的分布式锁完成,但是依然有些问题。
- 如在 key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。
- 就算 Redis 是集群部署的,如果每个节点都只是 master 没有 slave,那么 master 宕机时该节点上的所有 key 在那一时刻都相当于是释放锁了,这样也会出现并发问题。就算是有 slave 节点,但如果在数据同步到 salve 之前 master 宕机也是会出现上面的问题。
感兴趣的朋友还可以参考 Redisson 的实现。