一. 概述
二. Redis分布式锁简单实用
2.1 导入Redisson依赖包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
2.2 使用
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // cluster state scan interval in milliseconds
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002");
// 配置redis地址
RedissonClient redisson = Redisson.create(config);
// 获取实例
RLock lock = redisson.getLock("anyLock");
// 获取锁
lock.lock();
try {
// 执行业务代码块
...
} finally {
// 释放锁
lock.unlock();
}
三. 分析原理, 手写一个Redis锁
3.1 声明接口
/**
* 定义锁
* @author shaolq
*
*/
public interface DLock {
/**
* 获取锁
* @param lock 锁名称
*/
void lock(String lock);
/**
* 释放锁
* @param lock 锁名称
*/
void unlock(String lock);
}
3.2 接口实现类
/**
* 通过redis实现分布锁
*
* @author shaolq
*
*/
public class DistributeLock implements DLock {
private static final int LOCK_MAX_EXIST_TIME = 5; // 单位s,一个线程持有锁的最大时间
private static final String LOCK_PREX = "lock_"; // 作为锁的key的前缀
private StringRedisTemplate redisTemplate;
private String lockPrex; // 做为锁key的前缀
private int lockMaxExistTime; // 单位s,一个线程持有锁的最大时间
private DefaultRedisScript<Long> lockScript; // 锁脚本
private DefaultRedisScript<Long> unlockScript; // 解锁脚本
// 线程变量
private ThreadLocal<String> threadKeyId = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());
public DistributeLock(StringRedisTemplate redisTemplate){
this(redisTemplate, LOCK_PREX, LOCK_MAX_EXIST_TIME);
}
public DistributeLock(StringRedisTemplate redisTemplate, String lockPrex, int lockMaxExistTime){
this.redisTemplate = redisTemplate;
this.lockPrex = lockPrex;
this.lockMaxExistTime = lockMaxExistTime;
// init
init();
}
/**
* 生成
*/
public void init() {
// Lock script 导入获取锁的Lua脚本
lockScript = new DefaultRedisScript<Long>();
lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/lock.lua")));
lockScript.setResultType(Long.class);
// unlock script 导入释放锁的Lua脚本
unlockScript = new DefaultRedisScript<Long>();
unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/unlock.lua")));
unlockScript.setResultType(Long.class);
}
@Override
public void lock(String lock2){
Assert.notNull(lock2, "lock2 can't be null!");
String lockKey = getLockKey(lock2);
while(true){
List<String> keyList = Lists.newArrayList();
keyList.add(lockKey);
keyList.add(threadKeyId.get());
if(redisTemplate.execute(lockScript, keyList, String.valueOf(lockMaxExistTime * 1000)) > 0){
break;
}else{
try {
// 短暂休眠,nano避免出现活锁
Thread.sleep(10, (int)(Math.random() * 500));
} catch (InterruptedException e) {
break;
}
}
}
}
/**
* 释放锁,同时要考虑当前锁是否为自己所有,以下情况会导致当前线程失去锁:线程执行的时间超过超时的时间,导致此锁被其它线程拿走; 此时用户不可以执行删除
*
*/
@Override
public void unlock(final String lock) {
final String lockKey = getLockKey(lock);
List<String> keyList = Lists.newArrayList();
keyList.add(lockKey);
keyList.add(threadKeyId.get());
redisTemplate.execute(unlockScript, keyList);
}
/**
* 生成key
* @param lock
* @return
*/
private String getLockKey(String lock){
StringBuilder sb = new StringBuilder();
sb.append(lockPrex).append(lock);
return sb.toString();
}
}
3.3 编写获取Lua脚本
-- Set a lock
-- 如果获取锁成功,则返回 1
local key = KEYS[1] -- key
local content = KEYS[2] -- 线程id
local ttl = ARGV[1] -- 过期时间
-- setnx: 表示将 key 的值设为 content,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
-- 返回整数,具体为
--1,当 key 的值被设置
--0,当 key 的值没被设置
local lockSet = redis.call('setnx', key, content)
if lockSet == 1 then
-- 设置key的过期时间, 时间单位为毫秒
redis.call('pexpire', key, ttl)
-- redis.call('incr', "count")
else
-- 如果value相同,则认为是同一个线程的请求,则认为重入锁
local value = redis.call('get', key)
if(value == content) then
lockSet = 1;
redis.call('pexpire', key, ttl)
end
end
return lockSet
-- unlock key
local key = KEYS[1] -- key
local content = KEYS[2] -- 线程id
local value = redis.call('get', key)
-- 判断该key的值是否当前线程的值, 是则释放锁, 否则不处理
if value == content then
-- redis.call('decr', "count")
return redis.call('del', key);
end
return 0
3.4 注入bean
@Configuration
public class DistributedLockConfiguration {
@Resource(name = "stringRedisTemplate")
protected StringRedisTemplate stringRedisTemplate;
@Bean
public DistributeLock distributeLock() {
return new DistributeLock(stringRedisTemplate, "lock_", 1200);
}
}
四.不用LUA脚本实现分布式锁
public class RedisLockUtil {
private Logger log = LoggerFactory.getLogger(RedisDao.class);
@Resource
private StringRedisTemplate stringRedisTemplate;
// 当前线程唯一常量
private ThreadLocal<String> threadKeyId = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());
/**
* redis获取锁,获取不到就会不断重试
* @param key 锁名
* @param time 锁的最大有效时间(秒)
*/
public void lock(String key,int time){
// 锁名
String keyName = getLockName(key);
while(true){
// 争抢锁
Boolean flag = setnxAndExpire(keyName, threadKeyId.get(), time);
// 判断key是否存在
if(!flag){
// 判断是否重入锁
if(threadKeyId.get().equals(String.valueOf(stringRedisTemplate.opsForValue().get(keyName)))){
break;
}
// 短暂休眠,nano避免出现活锁
try {
Thread.sleep(500, 500);
} catch (InterruptedException e) {
break;
}
}else {
break;
}
}
}
/**
* 尝试获取锁,立即返回结果
* @param key
* @param time
* @return
*/
public Boolean tryLock(String key,int time){
// 锁名
String keyName = getLockName(key);
return setnxAndExpire(keyName, threadKeyId.get(), time);
}
/**
* 释放锁
* @param key
*/
public void unlock(String key){
// 锁名
String keyName = getLockName(key);
// 释放锁
if (threadKeyId.get().equals(stringRedisTemplate.opsForValue().get(keyName))) {
stringRedisTemplate.delete(keyName);
}
}
/**
* 获取锁的名字
* @param key
* @return
*/
public String getLockName(String key){
if(null != key){
return "REDIS_LOCK_"+key;
}else {
return "DEFAULT_LOCK";
}
}
/**
* key不存在时设置value,一般用于争抢锁
* 把setnx和expire合并,保证原子性,
* @param key
* @param value 值
* @param time 秒
* @return
*/
public Boolean setnxAndExpire(String key ,String value,long time){
/**
* set key value [EX seconds] [PX milliseconds] [NX|XX]
* EX seconds:设置失效时长,单位秒
* PX milliseconds:设置失效时长,单位毫秒
* NX:key不存在时设置value,成功返回OK,失败返回(nil)
* XX:key存在时设置value,成功返回OK,失败返回(nil)
*
* 示例: set name p7+ ex 100 nx
* SET操作成功后,返回的是OK,失败返回NIL
*/
return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
/**
* 给定命令与给定参数一起的“本机”或“原始”执行。 该命令按原样执行,并且尽可能少地执行“解释”,这取决于调用方对参数或结果的任何处理
*/
Object obj = connection.execute("set", SafeEncoder.encode(key), SafeEncoder.encode(value), SafeEncoder.encode("EX"), Protocol.toByteArray(time), SafeEncoder.encode("NX"));
return obj != null;
});
}
}