Redis分布式锁
基于jedis实现分布式锁,源码地址
配置文件
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
@Configuration
public class JedisConfig {
/**
* 设置redis服务器的host或者ip地址
*/
@Value("${universe.redis.hostName}")
private String hostName;
/**
* 设置redis的服务的端口号
*/
@Value("${universe.redis.port}")
private Integer port;
/**
* 超时时间 毫秒
*/
@Value("30000")
private int timeOut;
/**
* 资源池允许的最大空闲连接数
*/
@Value("${universe.redis.pool.maxIdle}")
private Integer maxIdle;
/**
* 当资源池用尽后,调用者是否要等待。只有当值为true时,
* 下面的maxWaitMillis才会生效。
*/
@Value("${universe.redis.pool.blockWhenExhausted}")
private Boolean blockWhenExhausted;
/**
* 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)。
*/
@Value("${universe.redis.pool.maxWaitMillis}")
private Long maxWaitMillis;
/**
* 是否开启JMX监控
*/
@Value("${universe.redis.pool.jmxEnabled}")
private Boolean jmxEnabled;
@Bean
public JedisPool jedisPoolFactory(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
jedisPoolConfig.setJmxEnabled(jmxEnabled);
return new JedisPool(jedisPoolConfig,hostName,port,timeOut);
}
}
分布式锁实现
redis中key的续期要用到延迟队列,所以定义一个实体类实现延迟队列操作,让延迟队列在锁过期前200毫秒时拿到队列元素。
/**
* @Author
* @Date 2024/5/18 22:30
* 延迟队列存储的对象
*/
public class ItemVo<T> implements Delayed {
/** 激活时间,表示activeTime毫秒之后可以取到数据*/
private long activeTime;
/** 业务数据,这里是redis中的key和value*/
private T data;
/** 延迟队列采用毫秒单位计时*/
public ItemVo(Long time, T data) {
/** 表示在redis中key的过期之前的100毫秒才能拿到数据,进行延期操作*/
this.activeTime = System.currentTimeMillis() + time -200;
this.data = data;
}
public Long getTime() {
return activeTime;
}
public T getData() {
return data;
}
/** 返回到激活元素的剩余时长*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(activeTime-System.currentTimeMillis(),unit);
}
/** 队列元素按剩余时长排序*/
@Override
public int compareTo(Delayed o) {
long between = getDelay(TimeUnit.MILLISECONDS)-o.getDelay(TimeUnit.MILLISECONDS);
return between == 0 ? 0 : between > 0 ? 1 : -1;
}
}
定义一个实体类存储key-value形式(使用map也可以)
/**
* @Author
* @Date 2024/5/18 22:31
* 存储redis中的key-value
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class LockItem {
private String key;
private String value;
}
定义一个lock类实现分布式锁,定义锁的过期时间为1S
/**
* @Author 邵博跃
* @Date 2024/5/15 21:22
*
* redis分布式锁
*/
@Component
public class RedisLock implements Lock {
/** 失效时间 */
private static final long LOCK_TIME = 1000;
/** key前缀 */
private static final String KEY_PREFIX = "test:";
/** 释放锁,确保原子性,确保释放锁的线程是枷锁的线程 RUA脚本 */
private static final String LOCK_RUA =
"if(redis.call('get', KEYS[1]) == ARGV[1]) then\n" +
" return redis.call('del', KEYS[1])\n" +
" end\n" +
" return 0";
/** 定义一个线程变量存储加锁线程的ID */
private ThreadLocal<String> threadLocal = new ThreadLocal<>();
/** 存储当前线程, 解决锁的可重入问题*/
private Thread currentThread;
/** 锁名称 */
private String lockName = "lock";
@Autowired
private JedisPool jedisPool;
/** -------守护线程参数------- */
/** 延迟队列*/
public final DelayQueue<ItemVo<LockItem>> delayQueue = new DelayQueue<>();
/** 锁失效时间转为为字符串,用于执行锁续期RUA脚本*/
public static final String LOCK_TIME_STR = String.valueOf(LOCK_TIME);
/**
* 续期RUN脚本
*/
private static final String DELAY_LOCK_RUA =
"if redis.call('get',KEYS[1]) == ARGV[1] then\n" +
" return redis.call('pexpire',KEYS[1],ARGV[2])\n" +
" else end return 0 ";
public String getLockName() {
return lockName;
}
public void setLockName(String lockName) {
this.lockName = lockName;
}
public void setCurrentThread(Thread currentThread) {
this.currentThread = currentThread;
}
/**
* 加锁
*/
@Override
public void lock() {
/** 加锁,如果加锁失败,阻塞100毫秒后重试 */
while (!tryLock()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
/**
* 加锁
*
* @return
*/
@Override
public boolean tryLock() {
//获取当前线程
Thread t = Thread.currentThread();
/**
* 先判断当前线程是不是持有锁的线程,如果是,实现可重入,返回true
* 如果不是,判断持有锁的线程是不是空,如果不是空,证明有其他线程持有锁,返回false
*
*/
if(t == currentThread){
return true;
}else if (currentThread != null){
return false;
}
Jedis jedis = jedisPool.getResource();
String id = null;
try {
id = UUID.randomUUID().toString();
SetParams setParams = new SetParams();
setParams.nx();
setParams.px(LOCK_TIME);
/** 本地线程枪锁 */
synchronized (this){
if(currentThread == null && "OK".equals(jedis.set(KEY_PREFIX + lockName,id, setParams))){
//持有锁的线程设置为当前线程
setCurrentThread(t);
//将set的Value放到线程变量
threadLocal.set(id);
System.out.println("加锁成功,uuid ->" + id);
//延迟队列塞数据
ItemVo<LockItem> lockItemItemVo = new ItemVo<>(LOCK_TIME, new LockItem(KEY_PREFIX + lockName, id));
long delay = lockItemItemVo.getDelay(TimeUnit.MILLISECONDS);
System.out.println("队列过期时间 -》"+delay);
delayQueue.add(lockItemItemVo);
//开启守护线程
/**
* 守护线程(看门狗)
*/
Thread daoThread = new Thread(() ->{
System.out.println("看门狗线程已启动");
//守护线程执行逻辑:只要用户线程未结束,守护线程就一直要运行,通过延迟队列来控制是否执行逻辑
while (!Thread.currentThread().isInterrupted()){
try {
//获取key-value
LockItem data = delayQueue.take().getData();
//续期操作
Long result = (Long) jedis.eval(DELAY_LOCK_RUA, Arrays.asList(data.getKey()), Arrays.asList(data.getValue(), LOCK_TIME_STR));
if(result.longValue() == 0L){
System.out.println("已释放锁,无需要延期!");
}else {
System.out.println("锁已续期->" + LOCK_TIME_STR);
//如果续期成功了,延迟队列再塞入一个元素用于下次续期使用
delayQueue.add(new ItemVo<>(LOCK_TIME,new LockItem(data.getKey(),data.getValue())));
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("锁续期失败"+e);
}finally {
if(jedis != null){jedis.close();}
}
}
});
daoThread.setDaemon(true);
daoThread.start();
return true;
}else {
return false;
}
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("加锁失败,uuid -> " + id + e.getMessage());
}finally {
jedis.close();//关闭jedis
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
/**
* 解锁
*/
@Override
public void unlock() {
Thread t = Thread.currentThread();
if(t != currentThread){
System.out.println("非持有锁线程,不能释放锁");
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String id = threadLocal.get();
Long eval = (Long)jedis.eval(LOCK_RUA, Arrays.asList(KEY_PREFIX + lockName), Arrays.asList(id));
if(eval != 0L){
System.out.println("解锁成功!uuid ->" + id);
}else {
System.out.println("解锁失败! uuid ->" + id);
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(jedis != null){
jedis.close();
}
threadLocal.remove();
setCurrentThread(null);
}
}
@Override
public Condition newCondition() {
return null;
}
}
测试
让执行业务逻辑代阻塞5S。
/** 使用Redis分布式锁 */
@Transactional
public void updateByRedisLock(Long id,int count){
try {
redisLock.lock();
Product product = mapper.selectById(id);
int newCont = product.getProductCount() - count;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(newCont < 0){
throw new RuntimeException("商品不足,扣除失败!");
}
product.setProductCount(newCont);
if(mapper.updateProduct(product) >0 ){
System.out.println("扣减成功!");
}else {
throw new RuntimeException("扣减失败");
}
}finally {
redisLock.unlock();
}
}
结果:续期6次
加锁成功,uuid ->4d31de91-e929-4596-b6d1-05b95c7255f8
队列过期时间 -》800
看门狗线程已启动
锁已续期->1000
锁已续期->1000
锁已续期->1000
锁已续期->1000
锁已续期->1000
锁已续期->1000
扣减成功!
解锁成功!uuid ->4d31de91-e929-4596-b6d1-05b95c7255f8
已释放锁,无需要延期!