前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【代码分享】基于Redis的SETNX操作实现的分布式锁

【代码分享】基于Redis的SETNX操作实现的分布式锁

作者头像
BUG弄潮儿
发布2022-06-30 16:10:59
4370
发布2022-06-30 16:10:59
举报
文章被收录于专栏:JAVA乐园

import com.jd.jim.cli.Cluster;

import java.io.IOException;

import java.util.concurrent.TimeUnit;

/**

* <pre>

* 基于Redis的SETNX操作实现的分布式锁

* </pre>

* @author lzc.java@icloud.com

*

*/

public class RedisDistributedLock {

private Cluster redis;

// 锁的名字

private String lockKey;

// 锁的值

private String lockVal = "";

// 默认锁的有效时长(毫秒)

private long lockExpires;

private boolean locked;

// 当前jvm内持有该锁的线程(if have one)

private Thread exclusiveOwnerThread;

/**

*

* @param redis

* @param lockKey lockKey

* @param lockExpires lockKey过期时间,单位:毫秒

* @throws IOException

*/

public RedisDistributedLock(Cluster redis, String lockKey, long lockExpires){

this.redis = redis;

this.lockKey = lockKey;

this.lockExpires = lockExpires;

}

/**

* 阻塞式获取锁 ,不过有超时时间,超过了tryGetLockTime还未获取到锁将直接返回false

* @param tryGetLockTime

* @param tryGetLockUnit

* @return

* @throws InterruptedException

*/

protected boolean lock(long tryGetLockTime, TimeUnit tryGetLockUnit){

try {

// 超时控制 的时间可以从本地获取, 因为这个和锁超时没有关系, 只是一段时间区间的控制

long start = System.currentTimeMillis();

long timeout = tryGetLockUnit.toMillis(tryGetLockTime);

//int tryTimes=0;

while (System.currentTimeMillis() - start < timeout) {

//tryTimes++;

//锁超时时间

long lockExpireTime = System.currentTimeMillis() + lockExpires + 1;

String stringOfLockExpireTime = String.valueOf(lockExpireTime);

if (setnx(lockKey, stringOfLockExpireTime)) { // 获取到锁

// 成功获取到锁, 设置相关标识

locked = true;

exclusiveOwnerThread = Thread.currentThread();

//System.out.println("拿到锁了,哈哈:"+tryTimes);

return true;

}

//说明未获取到锁,进一步检查锁是否已经超时

String lockVal=redis.get(lockKey);

//是存在lockVal=null的情况的,C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。

// C2通过SETNX向lockKey设置时间戳T0 发现有客户端已经获取锁,进入GET操作。

// 这时候C1客户端DEL掉锁成功。

// C2向lockKey发送GET命令,获取返回值T1(null)。

if(lockVal!=null&&Long.parseLong(lockVal)<System.currentTimeMillis()){

//表明已经超时了,原来的线程可能可能出现意外未能及时释放锁

String oldLockVal=redis.getSet(lockKey,stringOfLockExpireTime);

//为什么会有下面这个判断呢?因为多线程情况下可能同时有多个线程在这一时刻发现锁过期,那么就会同时执行getSet获取锁操作,

//通过下面的比较,可以找到第一个执行getSet操作的线程,让其获得锁,其它的线程则重试

//oldLockVal也存在null的情况,大家可以想想为什么

if(lockVal.equals(oldLockVal)){

redis.expire(lockKey, lockExpires, TimeUnit.MILLISECONDS);

// 成功获取到锁, 设置相关标识

locked = true;

exclusiveOwnerThread = Thread.currentThread();

return true;

}

}

Thread.sleep(5L);

}

} catch (InterruptedException e) {

e.printStackTrace();

}

return false;

}

/**

* 非阻塞,立即返回是否获取到锁

* @return

*/

public boolean tryLock() {

if (setnx(lockKey, lockVal)) { // 获取到锁

// 成功获取到锁, 设置相关标识

locked = true;

//setExclusiveOwnerThread(Thread.currentThread());

exclusiveOwnerThread = Thread.currentThread();

return true;

}

return false;

}

private boolean setnx(String lockKey, Object val) {

if (redis.setNX(lockKey, String.valueOf(val))) {

redis.expire(lockKey, lockExpires, TimeUnit.MILLISECONDS);

return true;

}

return false;

}

public boolean isLocked() {

return locked;

}

/**

* 释放锁

*/

public void unlock() {

// 检查当前线程是否持有锁

if (Thread.currentThread() != exclusiveOwnerThread) {

// 表明锁并非当前线程所持有,不应该由当前线程来释放锁

System.out.println("表明锁并非当前线程所持有,不应该由当前线程来释放锁exclusiveOwnerThread:" + exclusiveOwnerThread + ",Thread.currentThread():"+Thread.currentThread()+",lockKey" + lockKey);

return;

}

//gaohongtianluck 忽略了一个地方。用del命令释放锁,如果线程A获得锁之后运行太久,久到另已经获得的锁失效了。

// 这时线程B进来,取缔了A上的锁,线程B运行到一半的时候,这时线程A也运行完了,杀一个回马枪把原本以为获取到的锁给del,

// 实际上是B获得的锁,那么就会导致其他线程进来竞争,而B还以为自己独占锁

//回复Ffadsfoadfjaodjfalkd:我也在思考这个问题,我觉得有一种写法可以尽量避免。在锁的时候,如果锁住了,回传超时时间,作为解锁时候的凭证,解锁时传入锁的键值和凭证。我思考的解锁时候有两种写法:

//1、解锁前get一下键值的value,判断是不是和自己的凭证一样。但这样存在一些问题:

//1)get时返回null的可能,此时表示有别的线程拿到锁并用完释放

//2)get返回非null,但是不等于自身凭证。由于有getset那一步,当两个竞争线程都在这个过程中时,存在持有锁的线程凭证不等于value,而value是稍慢那一步线程设置的value。

//

//2、解锁前用凭证判断锁是否已经超时,如果没有超时,直接删除;如果超时,等着锁自动过期就好,免得误删别人的锁。但这种写法同样存在问题,由于线程调度的不确定性,判断到删除之间可能过去很久,并不是绝对意义上的正确解锁。

//

//关于解锁我只想到这么多,希望有帮助,欢迎拍砖多交流。

//综上所述,lzc.java实现采用了非常简单的方法,如上所述,即超时的情况下可能会出现误释放锁的场景,所以使用的时候就需要合理设置超时时间了

redis.del(lockKey);

exclusiveOwnerThread = null;

}

}

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 BUG弄潮儿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档