分布式锁(数据库、ZK、Redis)拍了拍你(1)

文章详细描述了使用Redis(包括setnx、getset、expire和del命令,以及Redisson库)和ZooKeeper(临时顺序节点)实现分布式锁的方法,强调了乐观锁机制和Redisson的lua脚本在加锁过程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

public void run() {

while (true) {

jedis = RedisUtil.getInstance().getJedis();

try {

jedis.watch(key);

int num= Integer.parseInt(jedis.get(key));// 当前商品个数

if (num> 0) {

Transaction ts= jedis.multi(); // 开始事务

ts.set(key, String.valueOf(num - 1)); // 库存扣减

List result = ts.exec(); // 执行事务

if (result == null || result.isEmpty()) {

System.out.println(“抱歉,您抢购失败,请再次重试”);

} else {

System.out.println(“恭喜您,抢购成功”);

break;

}

} else {

System.out.println(“抱歉,商品已经卖完”);

break;

}

} catch (Exception e) {

e.printStackTrace();

} finally {

jedis.unwatch(); // 解除被监视的key

RedisUtil.returnResource(jedis);

}

}

}

}

复制代码

在代码的实现中有一个重要的点就是**「商品的数据量被watch了」**,当前的客户端只要发现数量被改变就会抢购失败,然后不断的自旋进行抢购。

这个是基于Redis事务实现的简单的秒杀系统,Redis事务中的watch命令有点类似乐观锁的机制,只要发现商品数量被修改,就执行失败。

Redis原生命令实现

==============================================================================

Redis实现分布式锁的第二种方式,可以使用setnx、getset、expire、del这四个命令来实现。

  1. setnx:命令表示如果key不存在,就会执行set命令,若是key已经存在,不会执行任何操作。

  2. getset:将key设置为给定的value值,并返回原来的旧value值,若是key不存在就会返回返回nil 。

  3. expire:设置key生存时间,当当前时间超出了给定的时间,就会自动删除key。

  4. del:删除key,它可以删除多个key,语法如下:DEL key [key …],若是key不存在直接忽略。

下面通过一个代码案例是实现以下这个命令的操作方式:

public void redis(Produce produce) {

long timeout= 10000L; // 超时时间

Long result= RedisUtil.setnx(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));

if (result!= null && result.intValue() == 1) { // 返回1表示成功获取到锁

RedisUtil.expire(produce.getId(), 10);//有效期为5秒,防止死锁

//执行业务操作

//执行完业务后,释放锁

RedisUtil.del(produce.getId());

} else {

System.println.out(“没有获取到锁”)

}

}

复制代码

在线程A通过setnx方法尝试去获取到produce对象的锁,若是获取成功就会返回1,获取不成功,说明当前对象的锁已经被其它线程锁持有。

获取锁成功后并设置key的生存时间,能够有效的防止出现死锁,最后就是通过del来实现删除key,这样其它的线程就也可以获取到这个对象的锁。

执行的逻辑很简单,但是简单的同时也会出现问题,比如你在执行完setnx成功后设置生存时间不生效,此时服务器宕机,那么key就会一直存在Redis中。

当然解决的办法,你可以在服务器destroy函数里面再次执行:

RedisUtil.del(produce.getId());

复制代码

或者通过**「定时任务检查是否有设置生存时间」**,没有的话都会统一进行设置生存时间。

还有比较好的解决方案就是,在上面的执行逻辑里面,若是没有获取到锁再次进行key的生存时间:

public void redis(Produce produce) {

long timeout= 10000L; // 超时时间

Long result= RedisUtil.setnx(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));

if (result!= null && result.intValue() == 1) { // 返回1表示成功获取到锁

RedisUtil.expire(produce.getId(), 10);//有效期为10秒,防止死锁

//执行业务操作

//执行完业务后,释放锁

RedisUtil.del(produce.getId());

} else {

String value= RedisUtil.get(produce.getId());

// 存在该key,并且已经超时

if (value!= null && System.currentTimeMillis() > Long.parseLong(value)) {

String result = RedisUtil.getSet(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));

if (result == null || (result != null && StringUtils.equals(value, result))) {

RedisUtil.expire(produce.getId(), 10);//有效期为10秒,防止死锁

//执行业务操作

//执行完业务后,释放锁

RedisUtil.del(produce.getId());

} else {

System.println(“没有获取到锁”)

}

} else {

System.println(“没有获取到锁”)

}

}

}

复制代码

这里对上面的代码进行了改进,在获取setnx失败的时候,再次重新判断该key的锁时间是否失效或者不存在,并重新设置生存的时间,避免出现死锁的情况。

Redisson实现

=============================================================================

第三种Redis实现分布式锁,可以使用Redisson来实现,它的实现简单,已经帮我们封装好了,屏蔽了底层复杂的实现逻辑。

先来一个Redisson的原理图,后面会对这个原理图进行详细的介绍:

image

我们在实际的项目中要使用它,只需要引入它的依赖,然后执行下面的代码:

RLock lock = redisson.getLock(“lockName”);

lock.locl();

lock.unlock();

复制代码

并且它还支持**「Redis单实例、Redis哨兵、redis cluster、redis master-slave」**等各种部署架构,都给你完美的实现,不用自己再次拧螺丝。

但是,crud的同时还是要学习一下它的底层的实现原理,下面我们来了解下一下,对于一个分布式的锁的框架主要的学习分为下面的5个点:

  1. 加锁机制

  2. 解锁机制

  3. 生存时间延长机制

  4. 可重入加锁机制

  5. 锁释放机制

只要掌握一个框架的这五个大点,基本这个框架的核心思想就已经掌握了,若是要你去实现一个锁机制框架,就会有大体的一个思路。

Redisson中的加锁机制是通过lua脚本进行实现,Redisson首先会通过**「hash算法」**,选择redis cluster集群中的一个节点,接着会把一个lua脚本发送到Redis中。

它底层实现的lua脚本如下:

returncommandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,

"if (redis.call(‘exists’, KEYS[1]) == 0) then " +

"redis.call(‘hset’, KEYS[1], ARGV[2], 1); " +

"redis.call(‘pexpire’, KEYS[1], ARGV[1]); " +

"return nil; " +

"end; " +

"if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then " +

"redis.call(‘hincrby’, KEYS[1], ARGV[2], 1); " +

"redis.call(‘pexpire’, KEYS[1], ARGV[1]); " +

"return nil; " +

"end; " +

“return redis.call(‘pttl’, KEYS[1]);”,

Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

复制代码

「redis.call()的第一个参数表示要执行的命令,KEYS[1]表示要加锁的key值,ARGV[1]表示key的生存时间,默认时30秒,ARGV[2]表示加锁的客户端的ID。」

比如第一行中redis.call(‘exists’, KEYS[1]) == 0) 表示执行exists命令判断Redis中是否含有KEYS[1],这个还是比较好理解的。

lua脚本中封装了要执行的业务逻辑代码,它能够保证执行业务代码的原子性,它通过hset lockName命令完成加锁。

若是第一个客户端已经通过hset命令成功加锁,当第二个客户端继续执行lua脚本时,会发现锁已经被占用,就会通过pttl myLock返回第一个客户端的持锁生存时间。

若是还有生存时间,表示第一个客户端会继续持有锁,那么第二个客户端就会不停的自旋尝试去获取锁。

假如第一个客户端持有锁的时间快到期了,想继续持有锁,可以给它启动一个watch dog看门狗,他是一个后台线程会每隔10秒检查一次,可以不断的延长持有锁的时间。

Redisson中可重入锁的实现是通过incrby lockName来实现,「重入一个计数就会+1,释放一次锁计数就会-1」

最后,使用完锁后执行del lockName就可以直接**「释放锁」**,这样其它的客户端就可以争抢到该锁了。

这就是分布式锁的开源Redisson框架底层锁机制的实现原理,我们可以在生产中实现该框架实现分布式锁的高效使用。

下面通过一个多窗口抢票的例子代码来实现:

public class SellTicket implements Runnable {

private int ticketNum = 1000;

RLock lock = getLock();

// 获取锁

private RLock getLock() {

Config config = new Config();

config.useSingleServer().setAddress(“redis://localhost:6379”);

Redisson redisson = (Redisson) Redisson.create(config);

RLock lock = redisson.getLock(“keyName”);

return lock;

}

@Override

public void run() {

while (ticketNum>0) {

// 获取锁,并设置超时时间

lock.lock(1, TimeUnit.MINUTES);

try {

if (ticketNum> 0) {

System.out.println(Thread.currentThread().getName() + “出售第 " + ticketNum-- + " 张票”);

}

} finally {

lock.unlock(); // 释放锁

}

}

}

}

复制代码

测试的代码如下:

public class Test {

public static void main(String[] args) {

SellTicket sellTick= new SellTicket();

// 开启5五条线程,模拟5个窗口

for (int i=1; i<=5; i++) {

new Thread(sellTick, "窗口" + i).start();

}

}

}

复制代码

是不是感觉很简单,因为多线程竞争共享资源的复杂的过程它在底层都帮你实现了,屏蔽了这些复杂的过程,而你也就成为了优秀的API调用者。

上面就是Redis三种方式实现分布式锁的方式,基于Redis的实现方式基本都会选择Redisson的方式进行实现,因为简单命令,不用自己拧螺丝,开箱即用。

ZK实现的分布式锁

============================================================================

ZK实现的分布式锁的原理是基于一个**「临时顺序节点」**实现的,开始的时候,首先会在ZK中创建一个ParentLock持久化节点。

image

当有client1请求锁的时候,,就会在ParentLock下创建一个临时顺序节点,如下图所示:

image

并且,该节点是有序的,在ZK的内部会自动维护一个节点的序号,比如:第一个进来的创建的临时顺序节点叫做xxx-000001,那么第二个就叫做xxx-000002,这里的序号是一次递增的。

当client1创建完临时顺序节点后,就会检查ParentLock下面的所有的子节点,会判断自己前面是否还有节点,此时明显是没有的,所以获取锁成功。

image

当第二个客户端client2进来获取锁的时候,也会执行相同的逻辑,会先在创建一个临时的顺序节点,并且序号是排在第一个节点的后面:

image

并且第二部也会判断ParnetLock下面的所有的子节点,看自己是否是第一个,明显不是,此时就会加锁失败。

那么此时client2会创建一个对client1的lock1的监听(Watcher),用于监听lock1是否存在,同时client2会进入等待状态:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

知其然不知其所以然,大厂常问面试技术如何复习?

1、热门面试题及答案大全

面试前做足功夫,让你面试成功率提升一截,这里一份热门350道一线互联网常问面试题及答案助你拿offer

2、多线程、高并发、缓存入门到实战项目pdf书籍

3、文中提到面试题答案整理

4、Java核心知识面试宝典

覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入


《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
g-JGAhWNG5-1712363714557)]

[外链图片转存中…(img-yg8eHqM6-1712363714557)]

3、文中提到面试题答案整理

[外链图片转存中…(img-ZFtuhdGp-1712363714557)]

4、Java核心知识面试宝典

覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入

[外链图片转存中…(img-2JYF8Fov-1712363714558)]

[外链图片转存中…(img-GKFG4Ltw-1712363714558)]

[外链图片转存中…(img-rmLDKkHZ-1712363714558)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值