redis分布式锁的设计
今天我们来说一说基于redis分布式锁的设计(基于springboot框架下的实现):
1、首先我们设计一个接口:
/**
* 分布式锁
*
* @author fengjie song
*
*/
public interface DistributedLock {
/**
* 分布式锁1
*
* @param key
* @return
*/
boolean lock(String key);
/**
* 分布式锁2
*
* @param key
* @param expire
* @return
*/
boolean lock(String key, long expire);
/**
* 分布式锁3
*
* @param key
* @param expire
* @param retryTime
* @return
*/
boolean lock(String key, long expire, int retryTime);
/**
* 分布式锁4
*
* @param key redis key
* @param expire key的实效时间
* @param retryTime 获取锁的重试次数
* @param sleepMillis 睡眠时间(毫秒)
* @return
*/
boolean lock(String key, long expire, int retryTime, long sleepMillis);
/**
* 释放锁
*
* @param key
* @return
*/
boolean releaseLock(String key);
}
2、抽象类实现部分非核心的代码
/**
* 模板设计模式</br>
* 模板设计模式在这里可以有效避免方法多次实现,</br>
* 例如增加基于zookeeper的分布式锁,我们只需要重写核心的方法即可
*
* @author fengjie song
*
*/
public abstract class AbstractDistributedLock implements DistributedLock {
private static final long EXPIRE = 60;
private static final int RETRY_TIME = 3;
private static final long SLEEP_MILLIS = 1000;
@Override
public boolean lock(String key) {
return lock(key, EXPIRE, RETRY_TIME, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, long expire) {
return lock(key, expire, RETRY_TIME, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, long expire, int retryTime) {
return lock(key, expire, retryTime, SLEEP_MILLIS);
}
3、分布式锁实现类(基于redis),这里也可以设计基于zk等其他的分布式锁
/**
* redis分布式锁的实现(策略模式)</br>
* 这里只需要重写一个方法,其余方法在抽象类里面调用(模板设计模式)
*
* @author fengjie song
*
*/
public class RedisDistributedLock extends AbstractDistributedLock {
/**
* 引入redisTemplate
*/
private RedisTemplate<String, String> redisTemplate;
/**
* 记录redis锁的标识
*/
private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
/**
* 释放锁的LUA脚本
*/
private static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder(64);
sb.append(" redis.call('get',KEY[1]) == ARGV[1] ");
sb.append(" then ");
sb.append(" return redis.call('del',KEY[1]) ");
sb.append(" else ");
sb.append(" return 0 ");
sb.append(" end ");
UNLOCK_LUA = sb.toString();
}
public RedisDistributedLock(RedisTemplate<String, String> redisTemplate) {
super();
this.redisTemplate = redisTemplate;
}
@Override
public boolean lock(String key, long expire, int retryTime, long sleepMillis) {
// 设置锁
boolean result = setRedis(key, expire);
// 如果设置失败,根据设定的重试次数进行重试
while (!result && retryTime-- > 0) {
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
return false;
}
result = setRedis(key, expire);
}
return result;
}
private boolean setRedis(String key, long expire) {
String result = redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
// 这里我们不知道redis的连接方式是单机、读写分离的主从还是集群,所以在这里返回他们的父级接口JedisCommands
JedisCommands rc = (JedisCommands) connection.getNativeConnection();
String id = UUID.randomUUID().toString();
lockFlag.set(id);
return rc.set(key, id, "NX", "PX", expire);
}
});
return !StringUtils.isEmpty(result);
}
@Override
public boolean releaseLock(String key) {
try {
// 释放锁的时候,可能当前锁已经失效,对应的key锁可能已经被别的线程持有,所以不能直接删除锁,这里我们采用LUA脚本进行柔和删除
List<String> keys = new ArrayList<String>();
keys.add(key);
List<String> args = new ArrayList<String>();
args.add(lockFlag.get());
// 利用LUA脚本进行匹配key-value进行删除,可以避免因锁失效,而误删别的线程已经持有的锁
Long result = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object conn = connection.getNativeConnection();
// 虽然redis集群模式和单机模式有一样的接口,但是在这里无法确定是单机还是集群所以需要分开执行
if (conn instanceof Jedis) {
return (Long) ((Jedis) conn).eval(UNLOCK_LUA, keys, args);
}
if (conn instanceof JedisCluster) {
return (Long) ((JedisCluster) conn).eval(UNLOCK_LUA, keys, args);
}
return 0L;
}
});
return result != null && result > 0;
} finally {
// 清除过期或者已经删除的锁标识,避免内存溢出
lockFlag.remove();
}
}
}