一. 概述
在java开发中, 多线程环境下为了保证一个代码块在同一时间只能由一个线程访问, 我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式。但是现在公司都是流行分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?今天我们来介绍利用Redis实现分布式锁
二. 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锁
原理:
- 首先声明一个Key, 然后业务代码块执行前会找该key的值, 找不到就执行, 能找到就循环查找, 直到找不到再执行
- 找不都key值后, 先设置该key的值, 生成锁, 业务代码块执行完再删掉该key的值, 释放锁
- 为了避免死锁, 会给该key设一个有效时间
- 通常会把key的值设置为当前线程的唯一id, 当找到key的值, 再去比较该值与当前线程的唯一id是否相同, 相同则是重入锁可以执行, 否则等待
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脚本
- 获取锁Lua脚本 -> lock.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
- 释放锁Lua脚本 -> unlock.lua
-- 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);
}
}
网友评论