当前网上可以找到许多基于redis使用java实现的分布式锁的代码,其主要实现方式主要有以下几种:
1. SETNX、GETSET、GET、DEL
加锁时,使用SETNX设置锁名和锁的到期时间,若设置成功则获取锁;否则再检查锁是否已过期,是则使用GETSET设置新的到期时间,设置成功则获取到锁,获取到锁后记一下状态;解锁时,若锁已过期则直接解锁,否则根据状态判断是否由自己执有锁,是则解锁。
2. SETNX、EXPIRE、GET、DEL
加锁时,使用SETNX设置锁名和锁的执有者,使用EXPIRE设置锁的过期时间;解锁时,先查询锁是否还由锁执有者执有,是则直接DEL解锁。
这两种实现方式在实际代码中都会各种各样的问题,上代码来分析:
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import redis.clients.jedis.Jedis; public class WrongARedisDLock implements DLock { private static final Logger LOG = LogManager.getLogger(WrongARedisDLock.class); private final String lockName; private final int lockDuration; private final String lockSubject; private final Jedis jedis; public WrongARedisDLock(String lockName, int lockDuration, String lockSubject, Jedis jedis) { this.lockName = lockName; this.lockDuration = lockDuration; this.lockSubject = lockSubject; this.jedis = jedis; } public void lock() { tryLock(Long.MAX_VALUE); } public boolean tryLock() { return tryLock(0); } public boolean tryLock(long waitTimeout) { long startTime = System.currentTimeMillis(); while (true) { try { // 设置锁名和锁申请主体的唯一标识 if (jedis.setnx(lockName, lockSubject) != null) { // 若此时程序故障退出,锁将无法释放 jedis.expire(lockName, lockDuration); return true; } String lockOwner = jedis.get(lockName); if (lockSubject.equals(lockOwner)) { return true; } if (lockOwner == null) { continue; } if (System.currentTimeMillis() - startTime < waitTimeout) { try { Thread.sleep(100); } catch (InterruptedException ie) { } continue; } return false; } catch (Exception e) { LOG.error("tryLock error", e); if (System.currentTimeMillis() - startTime < waitTimeout) { try { Thread.sleep(100); } catch (InterruptedException ie) { } continue; } return false; } } } public void unlock() { try { String lockOwner = jedis.get(lockName); if (lockSubject.equals(lockOwner)) { // 若此时锁过期了并且被其它申请者成功获取,则将误删锁 jedis.del(lockName); } } catch (Exception e) { LOG.error("unlock error", e); } } }
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import redis.clients.jedis.Jedis; public class WrongBRedisDLock implements DLock { private static final Logger LOG = LogManager.getLogger(WrongBRedisDLock.class); private final String lockName; private final long lockDuration; private final String lockSubject; private final Jedis jedis; private boolean isLocked = false; public WrongBRedisDLock(String lockName, long lockDuration, String lockSubject, Jedis jedis) { this.lockName = lockName; this.lockDuration = lockDuration; this.lockSubject = lockSubject; this.jedis = jedis; } public void lock() { tryLock(Long.MAX_VALUE); } public boolean tryLock() { return tryLock(0); } public boolean tryLock(long waitTimeout) { long loopStartTime = System.currentTimeMillis(); while (true) { try { long startTime = System.currentTimeMillis(); String expireTimeStr = String.valueOf(startTime + lockDuration + 1); // 设置锁名和锁的到期时间,此处就存在多个不同进程或服务器当前时间可能会不一致的问题 if (jedis.setnx(lockName, expireTimeStr) != null) { isLocked = true; return true; } // 获取锁的过期时间 String oldExpireTimeStr = jedis.get(lockName); if (oldExpireTimeStr == null) { continue; } long oldExpireTime = Long.parseLong(oldExpireTimeStr); if (startTime > oldExpireTime) { // 在并发量较大时,可能会有多个锁申请主体同时进入到这里,并且都会修改锁的过期时间, // 这样会造成锁的实际过期时间比锁执有者设置的时间靠后 oldExpireTimeStr = jedis.getSet(lockName, expireTimeStr); if (oldExpireTimeStr != null) { // 以下代码可以保证第一个申请成功的主体获取到锁 oldExpireTime = Long.parseLong(oldExpireTimeStr); if (startTime > oldExpireTime) { isLocked = true; return true; } } } if (System.currentTimeMillis() - loopStartTime < waitTimeout) { try { Thread.sleep(100); } catch (InterruptedException ie) { } continue; } return false; } catch (Exception e) { LOG.error("tryLock error", e); if (System.currentTimeMillis() - loopStartTime < waitTimeout) { try { Thread.sleep(100); } catch (InterruptedException ie) { } continue; } return false; } } } public void unlock() { try { // 获取锁的到期时间 String oldExpireTimeStr = jedis.get(lockName); if (oldExpireTimeStr != null) { long oldExpireTime = Long.parseLong(oldExpireTimeStr); if (oldExpireTime < System.currentTimeMillis()) { // 若此时锁过期了并且被其它申请者成功获取,则将误删锁 jedis.del(lockName); } else { if (isLocked) { // 若此时锁过期了并且被其它申请者成功获取,则将误删锁 jedis.del(lockName); } } } isLocked = false; } catch (Exception e) { LOG.error("unlock error", e); } } }
正确的实现方式如下:
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class RedisDLock implements DLock { private static final Logger LOG = LogManager.getLogger(RedisDLock.class); private final String lockName; private final long lockDuration; private final String lockSubject; private final Jedis jedis; public RedisDLock(String lockName, long lockDuration, String lockSubject, Jedis jedis) { this.lockName = lockName; this.lockDuration = lockDuration; this.lockSubject = lockSubject; this.jedis = jedis; } public void lock() { tryLock(Long.MAX_VALUE); } public boolean tryLock() { return tryLock(0); } public boolean tryLock(long waitTimeout) { long startTime = System.currentTimeMillis(); while (true) { try { // 尝试加锁。等同于 SETNX + EXPIRE,通过设置一个属性值完成加锁过程 if (jedis.set(lockName, // 锁名 lockSubject, // 锁申请人 "NX", // 锁名不存在时插入 "PX", // 锁的有效时长的时间单位为millisecond lockDuration // 锁的有效时长 ) != null) { // 加锁成功 return true; } // 若加锁不成功,获取锁的拥有者。加锁不成功有可能是申请者已执有此锁,也可能是其他人执有此锁 String lockOwner = jedis.get(lockName); // 若锁执有者与锁申请者相同,则返回申请者已占用此锁 if (lockSubject.equals(lockOwner)) { return true; } // 若锁未被占用,则再次申请锁 if (lockOwner == null) { continue; } if (System.currentTimeMillis() - startTime < waitTimeout) { try { Thread.sleep(100); } catch (InterruptedException ie) { } continue; } // 返回锁已被其他申请主体占用 return false; } catch (Exception e) { LOG.error("tryLock error", e); if (System.currentTimeMillis() - startTime < waitTimeout) { try { Thread.sleep(100); } catch (InterruptedException ie) { } continue; } return false; } } } // 使用transaction解锁 public void unlock1() { try { jedis.watch(lockName); // 添加对锁的监控,若锁的相关属性发生变化,则整个事务将不执行 String lockOwner = jedis.get(lockName); if (lockSubject.equals(lockOwner)) { // 若锁未超时,则del操作会删除锁记录 // 若锁已超时,当前锁无执有者,则del操作不会造成影响 // 若锁已超时,当前锁被其他人执有,由于watch监控到锁的属性有变化,则事务中的del操作不会真正执行 Transaction tx = jedis.multi(); tx.del(lockName); tx.exec(); } else { jedis.unwatch(); // 若不再执有锁,则放开监控 } } catch (Exception e) { LOG.error("unlock error", e); } } // 使用lua script解锁 public void unlock() { try { String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end"; jedis.eval(script,Collections.singletonList(lockName),Collections.singletonList(lockSubject)); } catch (Exception e) { LOG.error("unlock error", e); } } }