《纵横高并发世界,Redis分布式锁让你秒杀无敌!》

本章学习目标:

  • 理解分布式锁原理并掌握使用
  • 理解乐观锁并掌握秒杀的实现
  • 理解Redisson的原理

分布式锁

Watch

利用Watch实现Redis乐观锁

乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。具体思路如下:

  1. 利用redis的watch功能,监控这个redisKey的状态值
  2. 获取redisKey的值
  3. 创建redis事务
  4. 给这个key的值+1
  5. 然后去执行这个事务,如果key的值被修改过则回滚,key不加1
Redis乐观锁实现秒杀

在这里插入图片描述

在这里插入图片描述

setnx

实现原理
  • 共享资源互斥
  • 共享资源串行化
  • 单应用中使用锁:(单进程多线程)
  • synchronized、ReentrantLock
  • 分布式应用中使用锁:(多进程多线程)
  • 分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
  • 利用Redis的单线程特性对共享资源进行串行化处理
实现方式

获取锁

  • 方式1(使用set命令实现)–推荐

在这里插入图片描述

  • 方式2(使用setnx命令实现) – 并发会产生问题

在这里插入图片描述

释放锁

  • 方式1(del命令实现) – 并发

在这里插入图片描述

问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?

答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

  • 方式2 (redis+lua脚本实现)–推荐

在这里插入图片描述

存在问题

单机

无法保证高可用

主–从

无法保证数据的强一致性,在主机宕机时会造成锁的重复获得。

在这里插入图片描述

无法续租

超过expireTime后,不能继续使用

本质分析

CAP模型分析

  • 在分布式环境下不可能满足三者共存,只能满足其中的两者共存,在分布式下P不能舍弃(舍弃P就是单机了)。
  • 所以只能是CP(强一致性模型)和AP(高可用模型)。
  • 分布式锁是CP模型,Redis集群是AP模型。 (base)
  • Redis集群不能保证数据的随时一致性,只能保证数据的最终一致性。

为什么还可以用Redis实现分布式锁?

  • 与业务有关
  • 当业务不需要数据强一致性时,比如:社交场景,就可以使用Redis实现分布式锁
  • 当业务必须要数据的强一致性,即不允许重复获得锁,比如金融场景(重复下单,重复转账)就不要使用
  • 可以使用CP模型实现,比如:zookeeper和etcd。

Redission分布式锁的使用

  • Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。
  • Redisson在基于NIO的Netty框架上,生产环境使用分布式锁。
加入jar包的依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>  
    <version>2.7.0</version>
</dependency>
配置Redisson

在这里插入图片描述

锁的获取和释放

在这里插入图片描述

业务逻辑中使用分布式锁

在这里插入图片描述

Redisson分布式锁的实现原理

在这里插入图片描述

加锁机制

如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。

发送lua脚本到redis服务器上,脚本如下:

在这里插入图片描述

lua的作用:保证这段复杂业务逻辑执行的原子性。
lua的解释:

  • KEYS[1])︰加锁的key
  • ARGV[1] : key的生存时间,默认为30秒
  • ARGV[2]:加锁的客户端ID (UUID.randomUUID( )+"+ threadld)

第一段if判断语句,就是用"exists myLock"命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。

  • 如何加锁呢?很简单,用下面的命令: hset myLock
  • 8743c9cO-0795-4907-87fd-6c719a6b4586:1 1
  • 通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:myLock :{“8743c9co-0795-4907-87fd-6c719a6b4586:1”:1}
  • 接着会执行"pexpire myLock 30000"命令,设置myLock这个锁key的生存时间是30秒。
锁互斥机制

那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?

很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。

接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间。比如还剩15000毫秒的生存时间。

此时客户端2会进入一个while循环,不停的尝试加锁。

自动延时机制

只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

可重入锁机制

第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。

第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

此时就会执行可重入加锁的逻辑,他会用:

incrby myLock

8743c9c0-0795-4907-87fd-6c71a6b4586:1 1

通过这个命令,对客户端1的加锁次数,累加1。数据结构会变成:

myLock :{“8743c9c0-0795-4907-87fd-6c719a6b4586:1”:2 }

释放锁机制

执行lua脚本如下:

在这里插入图片描述

– KEYS[1] :需要加锁的key,这里需要是字符串类型。

– KEYS[2] :redis消息的ChannelName,一个分布式锁对应唯一的一个channelName:

“redisson_lockchannel{” + getName() + “}”

– ARGV[1] :reids消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。

– ARGV[2] :锁的超时时间,防止死锁

– ARGV[3] :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。

如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:

“del myLock”命令,从redis里删除这个key。

然后呢,另外的客户端2就可以尝试完成加锁了。

分布式锁特性

  • 互斥性
    • 任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
  • 同一性
    • 锁只能被持有该锁的客户端删除,不能由其它客户端删除。
  • 可重入性
    • 持有某个锁的客户端可继续对该锁加锁,实现锁的续租
  • 容错性
    • 锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁

分布式锁的实际应用

数据并发竞争

利用分布式锁可以将处理串行化,前面已经讲过了。

防止库存超卖

在这里插入图片描述

订单1下单前会先查看库存,库存为10,所以下单5本可以成功;

订单2下单前会先查看库存,库存为10,所以下单8本可以成功;

订单1和订单2 同时操作,共下单13本,但库存只有10本,显然库存不够了,这种情况称为库存超卖。

可以采用分布式锁解决这个问题。

在这里插入图片描述

订单1和订单2都从Redis中获得分布式锁(setnx),谁能获得锁谁进行下单操作,这样就把订单系统下单

的顺序串行化了,就不会出现超卖的情况了。伪码如下:

在这里插入图片描述

注意此种方法会降低处理效率,这样不适合秒杀的场景,秒杀可以使用CAS和Redis队列的方式。

Zookeeper分布式锁的对比

  • 基于Redis的set实现分布式锁
  • 基于zookeeper临时节点的分布式锁

在这里插入图片描述

  • 基于etcd实现

三者的对比,如下表

在这里插入图片描述

分布式集群架构中的session分离

传统的session是由tomcat自己进行维护和管理,但是对于集群或分布式环境,不同的tomcat管理各自的session,很难进行session共享,通过传统的模式进行session共享,会造成session对象在各个tomcat之间,通过网络和Io进行复制,极大的影响了系统的性能。

可以将登录成功后的Session信息,存放在Redis中,这样多个服务器(Tomcat)可以共享Session信息。

利用spring-session-data-redis(SpringSession),可以实现基于redis来实现的session分离。

  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java-You

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值