java redis锁_Java中Redis锁的实现[通俗易懂]

java redis锁_Java中Redis锁的实现[通俗易懂]由于具体业务场景的需求,需要保证数据在分布式环境下的正确更新,所以研究了一下Java中分布式锁的实现。Java分布式锁的实现方式主要有以下三种:数据库实现的乐观锁Redis实现的分布式锁Zookeeper实现的分布式锁其中,较常用的是前两种方式,但是数据库实现方式需要较多的数据库操作,所以最终选择的是用Redis实现分布式锁。最初考虑分布式锁的数据安全性的时候,只考虑到两点。第一,Redis锁需要…

大家好,又见面了,我是你们的朋友全栈君。

d6f4303c369f

由于具体业务场景的需求,需要保证数据在分布式环境下的正确更新,所以研究了一下Java中分布式锁的实现。

Java分布式锁的实现方式主要有以下三种:

数据库实现的乐观锁

Redis实现的分布式锁

Zookeeper实现的分布式锁

其中,较常用的是前两种方式,但是数据库实现方式需要较多的数据库操作,所以最终选择的是用Redis实现分布式锁。

最初考虑分布式锁的数据安全性的时候,只考虑到两点。第一,Redis锁需要有一个超时时间,这样即便某个持有锁的节点挂了,也不到导致其他节点死锁,保证每个锁有一个UniqueId;第二,每个锁需要有一个UniqueId,确保当一个线程执行完一个任务去释放锁的时候释放的一定是自己的锁,否则可能存在一种场景,就是一个线程释放锁的时候,它的锁可能已经超时被释放了,而因为缺少一个UniqueId,它却释放了另一个线程的锁

基于以上两点的考虑,分别设计了获取锁和释放锁的api。

public interface DistributionLockService {

/**

* @param lockName the name of the lock

* @param uniqueCode uniqueCode for the lock

* @param expireTime expire time of lock(MILLISECONDS)

* @return the result of get lock

* */

boolean getLock(String lockName, String uniqueCode, int expireTime);

/**

* @param lockName the name of the lock

* @param uniqueCode uniqueCode for the lock

* */

void releaseLock(String lockName, String uniqueCode);

}

具体的实现代码如下:

/**

* @param lockName the name of the lock

* @param uniqueCode uniqueCode for the lock

* @param expireTime expire time of lock(MILLISECONDS)

* @return the result of get lock

* */

@Override

public boolean getLock(String lockName, String uniqueCode, int expireTime) {

boolean isLock = false;

try {

Long result = jedis.setnx(lockName, uniqueCode);

isLock = result == 1 ? true : false;

if (isLock) {

jedis.expire(lockName, expireTime);

}

} catch (Exception e){

logger.error(“DistributionLockService/getLock”, e);

}

return isLock;

}

/**

* @param lockName the name of the lock

* @param uniqueCode uniqueCode for the lock

* */

@Override

public void releaseLock(String lockName, String uniqueCode) {

try {

String tag = jedis.get(getKey(lockName));

if (tag != null && tag.equals(uniqueCode))

jedis.del(getKey(lockName));

} catch (Exception e) {

logger.error(“DistributionLockService/releaseLock”, e);

}

}

上述的代码用setnx+expire实现分布式锁。调用setnx,当传入的key未被占用时,就在redis中插入一条该key的记录,返回值为1,此时为其设置超时时间。而当这个key在redis中已有记录时,则不会重新插入记录,这样的话,便可以实现分布式锁的基本功能。且为其设置过期时间,并加入UniqueId的check,避免了上述提及的两个问题。

但是,上述代码仍然存在问题,就是忽略了操作的原子性。获取锁的时候,调sexnx方法与设置超时时间expire不是原子操作,如果在sexnx方法执行成功后,节点突然down掉,没有执行expire方法,而之后的释放锁操作也没有执行,那么这个节点便会长期持有锁,尽管这种可能性很小,但是依然存在死锁的风险。为了避免这种风险,修正代码如下:

private static final String SET_IF_NOT_EXIST = “NX”;

private static final String SET_WITH_EXPIRE_TIME = “PX”;

private static final String IS_LOCKED = “OK”;

/**

* @param lockName the name of the lock

* @param uniqueCode uniqueCode for the lock

* @param expireTime expire time of lock

* @return the result of get lock

* */

@Override

public boolean getLock(String lockName, String uniqueCode, int expireTime) {

boolean isLock = false;

try {

String result = jedis.set(lockName, uniqueCode, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

isLock = IS_LOCKED.equalsIgnoreCase(result) ? true : false;

} catch (Exception e){

logger.error(“DistributionLockService/getLock”, e);

}

return isLock;

}

Redis较高的版本中,有一个有五个参数的set方法,其中前两个参数就是key和value,最后一个参数是过期时间,中间两个参数表示setnx和setex,实际上就是一个可以设置过期时间的setnx方法。这个方法可以保证加锁和设置过期时间两者是作为一个请求传送到Redis服务器的,所以不会出现上述的死锁场景。

加锁的问题解决了,解锁的问题依然在。上述的解锁代码中,在解锁之前先验证了UniqueId,然后采用del方法来释放锁,但是由于get和del是两次请求,而不是一个原子操作,所以这之间仍存在并发的问题。若做check的时候,检查得到确实是这个锁的UniqueId,但是在执行del方法之前,这个锁已经超时,然后新的线程也已经获取到锁了,那么del删掉的锁,便不是自己的锁,而是下一个线程的锁。

Redis中没有直接的api处理这个问题。解决这个问题,需要使用lua脚本,来确保整个操作的原子性。代码如下:

/**

* @param lockName the name of the lock

* @param uniqueCode uniqueCode for the lock

* */

@Override

public void releaseLock(String lockName, String uniqueCode) {

try {

String script = “if redis.call(‘get’, KEYS[1]) == ARGV[1] ” +

“then return redis.call(‘del’, KEYS[1]) ” +

“else return 0 end”;

jedis.eval(script, Arrays.asList(lockName), Arrays.asList(uniqueCode));

} catch (Exception e) {

logger.error(“DistributionLockService/releaseLock”, e);

}

}

jedis的eval方法支持执行lua脚本方法,所以便可利用这个方法来实现释放锁的原子操作,具体逻辑和之前的代码其实是一致的,但是由于是原子操作,所以可以避免上文中存在的问题。

至此,简单Redis锁的实现便算是成功了。但是其中依然存在许多问题,如果Redis不是单机的,而是集群分布的,那么其中的数据同步该怎么做?在有些较看重数据的正确性的场景中,即使Redis锁超时,只要检测到机器仍在正常运行Redis锁就不应该被释放,而应该被续期,这些,都是redis锁在更复杂的场景中所需要考虑的。留待以后继续研究。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/149708.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • pycharm常见镜像源「建议收藏」

    pycharm常见镜像源「建议收藏」清华:https://pypi.tuna.tsinghua.edu.cn/simple阿里云:http://mirrors.aliyun.com/pypi/simple/中国科技大学:https://pypi.mirrors.ustc.edu.cn/simple/使用:pipinstall-i镜像源包…

    2022年8月27日
    3
  • 一对兔子从出生后第三个月起每个月_兔子繁衍问题python

    一对兔子从出生后第三个月起每个月_兔子繁衍问题python7-6 兔子繁衍问题 (20分)一对兔子,从出生后第3个月起每个月都生一对兔子。小兔子长到第3个月后每个月又生一对兔子。假如兔子都不死,请问第1个月出生的一对兔子,至少需要繁衍到第几个月时兔子总数才可以达到N对?输入格式: 输入在一行中给出一个不超过10000的正整数N。输出格式: 在一行中输出兔子总数达到N最少需要的月数。 输入样例: 30 输出样例: 9#includ…

    2022年8月18日
    4
  • 鼠标滚轮编码器工作原理_速度编码器工作原理

    鼠标滚轮编码器工作原理_速度编码器工作原理鼠标滚轮一旦出现滚动跳动,不连贯,基本都要换,修鼠标会经常遇到,好奇之下想了解一下这个小东西的原理。滚轮一端插在这个转盘里面,我们滚动滚轮时候,转盘被带动旋转,产生脉冲信号,电脑依靠这个信号判断滚轮的旋转方向和速度。我们拆一个机械编码器来看看。就是这个小东西,特别简单有没有,一共就4个零件最左边是铁壳,上面一般会有厂家信息,安装高度,和寿命等比如这个,安装高度10毫米,寿命500万圈。PS:一般普通的鼠标,都是选用安装高度为11mm,但还是要自己量清楚。这里需要注意的是,安装

    2022年9月30日
    0
  • vue之解决跨域问题[通俗易懂]

    vue之解决跨域问题[通俗易懂]同源策略:http协议、主机名、端口号都要相同。因为浏览器同源策略的影响,向后端服务器请求数据的时候,不能进行访问。可以采用代理服务器的方式,代理服务器:浏览器向一个相同同源策略的g代理服务器上请求资源,因为服务器之间没有同源策略,代理服务器就去找后端服务器请求资源,在返回给浏览器解决方法一:在根目录下新建vue.config.js文件,这里是js文件哈。module.exports={ lintOnSave:false,//取消格式化 devServer:{ proxy:

    2022年9月17日
    0
  • 【Python实践-1】求一元二次方程的两个解[通俗易懂]

    【Python实践-1】求一元二次方程的两个解[通俗易懂]知识点:importsys,sys模块包含了与Python解释器和它的环境有关的函数。“sys”是“system”的缩写。sys.exit()中途退出程序,(注:0是正常退出,其他为不正常

    2022年7月5日
    20
  • Redis 客户端连接

    Redis 客户端连接Redis命令用于在redis服务上执行操作。要在redis服务上执行命令需要一个redis客户端。Redis客户端在Redis包中有提供,这个包在我们前面的安装教程中就有安装过了。Redis通过监听一个TCP端口或者Unixsocket的方式来接收来自客户端的连接,当一个连接建立后,Redis内部会进行以下一些操作:首先,客户端socket会被设置

    2022年6月10日
    43

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号