Redis因为服务端的串行化执行命令的特性,又提供了很多原子的get/set的命令,所以很适合用来实现分布式锁。网上的实现很多,但大部分都是很久之前的,下面提供一个使用spring-data-redis
的实现的比较完整的。
分布式锁的要求
提供阻塞和非阻塞的获取锁接口
锁超时自动释放
锁超时被其他客户端获取后,原来的锁持有者不能释放锁
Redis实现
基于以上的要求,redis中实现方式是:
加锁:通过set一个key并设置一个超时。加锁成功后,返回一个token,释放锁时需提供该token。
释放锁:首先获取当前key的值,如果key的值和持有的token相同,则删除key,否则不做任何事情。
代码实现
加锁
public String tryLock(String name, long expire) { Assert.notNull(name, "Lock name must not be null"); Assert.isTrue(expire>0, "expire must greater than 0"); String token = UUID.randomUUID().toString(); //生成唯一的token //获取redis连接 RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection conn = factory.getConnection(); try{ //原子操作,如果key不存在则set,并设置超时时间 Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")), Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT); if(result!=null && result) return token; }finally { //释放连接 RedisConnectionUtils.releaseConnection(conn, factory); } return null; }
首先生成一个token,用来以后做锁的释放。
然后借助redis的SET key value PX milliseconds NX命令来设置锁,这条命令可以在一个原子操作中实现,如果key不存在则set,并且设置一个expire的时间。RedisConnection类提供了这个命令的接口。命令返回成功,则返回token,否则返回NULL,表明获取锁失败。
最后,因为我们自己获取的Connection,所以别忘记释放连接。
【注意】这里的使用UUID.randomUUID()方法生成的token并不能保证一定唯一。但是对于一般的系统是够用的,主要是因为如果两次生成的token是一样的话,只对一个情况有影响,就是获取锁的线程1没有及时释放锁,锁自动超时,然后线程2获取到锁,刚好线程2在获取锁时生成的token跟线程1是一样的。这个时候线程1来释放就会把不属于它的锁的释放掉。这个概率是非常小的。如果你的系统对于唯一性非常敏感的话,需要换个token生成方式,保证唯一。
释放锁
public boolean unlock(String name, String token) { Assert.notNull(name, "Lock name must not be null"); Assert.notNull(token, "Token must not be null"); byte[][] keysAndArgs = new byte[2][]; keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8")); keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8")); RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection conn = factory.getConnection(); try { Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs); if(result!=null && result>0) return true; }finally { RedisConnectionUtils.releaseConnection(conn, factory); } return false; }
释放锁需要三步,get->compare->del。由于redis中没有提供一个原子命令一次把这三件事做了,所以要借助lua脚本来实现,脚本如下(unlockScript变量):
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
阻塞式加锁
我们系统中加锁一般都有需求,如果获取不到就阻塞一段时间。所以我们可以加一个这种接口,其实很简单,就是不断地循环调用tryLock,直到获取到或者超过时间。
public String lock(String name, long expire, long timeout) throws InterruptedException { //限制阻塞时间,根据自己的业务系统设置。如果尝试加锁的线程多的话最好不要设置的太大,要不然会有太多的线程在自旋,耗费CPU Assert.isTrue(timeout>0 && timeout<60000, "timeout must greater than 0 and less than 1 min"); long startTime = System.currentTimeMillis(); String token; do{ token = tryLock(name, expire); if(token == null) { //加锁失败 if((System.currentTimeMillis()-startTime) > (timeout-50)) break; //超过阻塞时间则跳出 Thread.sleep(50); //等待50ms再试,线程多的话这个值不建议设置的太小 } }while(token==null); return token; }
【注】上面有两个时间值,一个是限制客户端最大阻塞时间,一个是每次自旋前的sleep时间,需要根据自己的业务情况调整。
锁的使用
锁的使用就注意一点,将unlock放到finally
String token = null; try{ token = redisLock.tryLock("lock_name", 10000); if(token != null) ... //业务代码 } finally { if(token!=null) redisLock.unlock("lock_name", token); }
Redis 2.6.12之前版本加锁实现
如果你的redis老到连lua都不支持(是不是需要跟运维好好谈谈了),当然方法也不是没有,就是直接使用上面两个命令。这样就有可能出现setnx成功expire失败的情况,造成锁一直存在,我们可以用下面的方法解决:
public String tryLock(String name, long expire) { Assert.notNull(name, "Lock name must not be null"); Assert.isTrue(expire>0, "expire must greater than 0"); String token = UUID.randomUUID().toString(); boolean result = redisTemplate.opsForValue().setIfAbsent(name, token); if(result) {//成功则设置ttl redisTemplate.expire(name, expire, TimeUnit.MILLISECONDS); return token; }else { //失败则检查一下锁有没有设置ttl,没有则设置上 long milliSec = redisTemplate.getExpire(name, TimeUnit.MILLISECONDS); if (milliSec <= 0) { redisTemplate.expire(name, expire, TimeUnit.MILLISECONDS); } return null; } }
显然上面的方法要繁琐很多,看看就行了,升级redis才是正道。
【附完整代码】
@Repository public class RedisLock { private static final String unlockScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; private StringRedisTemplate redisTemplate; public RedisLock(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public String lock(String name, long expire, long timeout) throws InterruptedException { Assert.isTrue(timeout>0 && timeout<60000, "timeout must greater than 0 and less than 1 min"); long startTime = System.currentTimeMillis(); String token; do{ token = tryLock(name, expire); if(token == null) { if((System.currentTimeMillis()-startTime) > (timeout-50)) break; Thread.sleep(50); //try 50 per sec } }while(token==null); return token; } public String tryLock(String name, long expire) { Assert.notNull(name, "Lock name must not be null"); Assert.isTrue(expire>0, "expire must greater than 0"); String token = UUID.randomUUID().toString(); RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection conn = factory.getConnection(); try{ Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")), Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT); if(result!=null && result) return token; }finally { RedisConnectionUtils.releaseConnection(conn, factory); } return null; } public boolean unlock(String name, String token) { Assert.notNull(name, "Lock name must not be null"); Assert.notNull(token, "Token must not be null"); byte[][] keysAndArgs = new byte[2][]; keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8")); keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8")); RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection conn = factory.getConnection(); try { Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs); if(result!=null && result>0) return true; }finally { RedisConnectionUtils.releaseConnection(conn, factory); } return false; } }
springboot依赖,也可以用spring-data-redis加jedis或者lettuce
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.0.5.RELEASE</version> </dependency>
还没有评论,来说两句吧...