项目-01-Redis单机情况分布式锁实现案例-功能描述
项目地址:GitHub
**内容简介: ** 基于用户购买商品这一情景, 简单给出商品超卖,重买等问题的解决方案,即加锁.
PS: 若文章字体偏大或者偏小,建议通过 ctrl键+鼠标滑轮 进行修改,以提升阅读效果.(带来不便,请谅解!)
Version:
JDK : 1.8
Redis : 6.0.9
前提须知:
本机状况:
- OS : Windows 10 专业版
- CPU: 15 4210m 2.6Ghz
- 内存: 12G
项目部署情况:
虚拟机:
- OS : CentOS 7 64 bit
- 内存: 1.5G
Redis 以及Nginx 安装在本虚拟机中.
项目描述:
问题复现:
以用户购买 商品编号为goods:01(默认为100个) 商品 为例,
在不加锁的情况下会出现重复购买同一商品现象, 在加锁的情况下, 可以解决重买问题,但是因为加了锁,会导致性能下降.
单机情况:
不加锁:
代码实现:
- GoodsController.class
- GoodsService.class
- 情景: 100个用户 ,不加锁, 购买商品编号为goods:01 的商品,
http://localhost:1111/buy_goods/goods:01
结果:
出现重买(一件商品卖给多个人)现象, 按理说的话,100 间商品,100人购买,商品最后的数量是0才对.
Redis数据库数据如图所示, 100人购买同一商品,商品出现了剩余,说明购买过程中出现了重复购买的情况. 即上图 所示.
吞吐量: 97.6/s
加锁:
代码实现:
- synchronized: GoodsController.class
- ReentrantLock: GoodsController.class
请求地址: http://localhost:1111/sync/buy_goods/goods:01
- 情景再现: 100 个用户, 购买goods:01 商品
- 测试案例按照Synchronized实现测试
结果:
Redis数据库查询结果为:
后台打印输出为:
吞吐量: 87.6/s
结论:
加了synchronized 锁以后, 解决了重买问题,但是系统吞吐量从97/s 降到了87/s, 说明系统性能确实有所下降.
分布式情况:
修改 windows 系统的Hosts文件, 将 虚拟机Ip地址映射成 mycentos.com , 以下访问内容基于此实现.
系统架构:
不加锁:
http 请求: http://mycentos.com/sync/buy_goods/goods:01
- 情景: 100 个用户, 购买商品编号goods:01 的商品(商品有100 个)
结果:
结果1 : 商品 goods:01 并没有卖完
由两图可知, 系统一共打印了100 条信息(说明不存在请求超时的请求–即每一个请求都成功获得了锁), 然后商品剩余11件, 加上图中红框标记内容,说明 商品存在超卖.
结果2: 商品出现超卖现象
结论:
Synchronized 可以解决单机情况下商品超卖问题,但不可以解决分布式情况下商品超卖问题, 因此,对于分布式应用,若要保证商品购买不出现超卖现象, 需要加分布式锁.
加锁:
如何加锁?
解决方案:
根据不加分布式锁出现商品超卖现象, 笔者决定给项目添加加分布式锁(利用redis 固定一个对象作为锁对象).
Redis 官网给了两种实现方式:
采用setnx加锁,lua 脚本解锁来实现分布式锁.
采用Redisson–Redlock 算法实现分布式锁.
原子操作SetNx 实现分布式锁
实现依据:
Redis单机情况下实现分布式锁
http://mycentos.com/redis/setnx/buy_goods/goods:01
- 情景一 : 100 个用户, 购买商品编号goods:01 的商品(商品有100 个)
代码实现:
RedisSetNxGoodsController.class
情景一结果:
由图知boot-redis-1111: 一共打印了40 条数据, boot-redis-2222: 一共打印了47调数据, 然后redis中商品goods:01剩余13件, 40+47+13=100,说明, 存在部分请求没有成功获取到锁,并且经校验后发现,没有商品存在超卖现象(注意,我们现在是100个请求,去买商品, 根据打印行数是可以判断商品超卖的).
出现此问题的原因:
最开始我以为是,线程数量太少(所以选择加大线程数,然后fail),实则是自己Jmeter 线程组设置出现错误, Ramp-Up Period 表示每秒启动的线程数, 就本例而言, 0(默认值) 表示立刻生成100 个线程, 自己的运行结果树上图所示, 部分http请求直接挂掉了.
自己最后的设置是Ramp-up Period =10, 线程数100, 然后程序就正常执行了.
最重要的一点, 我在这个案例中并没有出现商品超卖这个问题,所以,这个只能算是自己对于测试软件Jmeter认知不够充分而已.
补充情景: 就是在ramp-up period =10, thread nums=100 的情况下实现
- 情景二: 300 个用户,去抢购编号为goods:01 的 商品(商品数量为100个), 然后判断商品是否存在超卖现象(判断方法,人眼比对).
情景二结果:
由上图表示说是, 49+51=100,(boot-redis-1111:49 件, boot-redis-2222:51 件), 经过比对后,没有出现商品goods:01的超卖情况.
吞吐量:100/s
100/s
可能出现的问题:
此锁必须设置过期时间,否则若应用1获取锁后,应用1直接宕机,导致应用1一直持有这把锁,其他应用不可以获取锁了.
不同线程删除不属于他的锁该怎么办?
解决方案: 删除前判断锁是否属于当前线程, 若属于当前线程,则可以正常删除, 若不是,不做处理.
实际场景中,在线程获取锁后,未在锁超期时间内完成事务, 如何实现锁续期问题? 锁的有效时间又该如何设计呢?
结论:
在单机Redis情况下, 利用set nx +超时时间 进行加锁, 以及用lua脚本方式进行解锁(补充: 也可以利用Redis事务的方式来实现, 不过,推荐lua脚本, 原子性), 可以实现分布式锁,但是,若线程A在规定时间内(Redis分布式锁超期时间)内未完成业务,线程A不得不放弃业务执行,显然,这是不人性的,所以引出Redisson (RedLock 算法)来解决这个问题.
Redission 实现分布式锁:
理论依据:
//RedissonLock.scheduleExpirationRenewal(threadId);
// RedissonLock.renewExpiration(threadId);
Redisson 加锁解锁: 通过lua脚本实现.
开了一个线程, 不断续约直到 业务代码执行完成.
代码实现:
代码实现过程中,我给出了Redisson 锁的三种测试案例.
// lock.lock();// 自动续期 100 个线程 测试成功
// lock.lock(30, TimeUnit.SECONDS); //success,假设业务时间为30s, 有点长.
// lock.lock(300,TimeUnit.MILLISECONDS);// fail 锁时间过短,报错
RedissonController.class
情景:
request请求: http://mycentos.com/redisson/buy_goods/goods:01
100 个用户, 购买商品编号goods:01 的商品(商品有100 个)
结果:
由图知, boot-redis-1111, boot-redis-2222 分别处理了50个请求, 未出现超卖现象.
吞吐量:
结论:
Redis 单机情况下实现分布式锁,保证锁可以自动续期 ,Redisson 给出了完美解决方案, 建议是如果业务代码执行时间可以固定,可以选择给出leaseTime(线程持有锁的最大时间),若不能,交给Redisson去实现锁续期.
但是在Redis集群环境下,Redis分布式锁会存在各种各样的问题,下面给出两篇文章可以帮助你进一步的理解分布式锁.
具体参考:
- 基于Redis的分布式锁到底安全吗(上)?
- 基于Redis的分布式锁到底安全吗(下)?
疑问 ?
Redisson实现的分布式锁是否整的安全?
答: 在Redis单机情况下,Redisson分布式锁是安全的, 但是在集群Redis环境中,分布式锁存在问题.
总结:
吞吐量的测试: 纯粹是自己闹着玩的,别当真,当真你就输了(1轮测试中吞吐量大,多轮测试后吞吐量呈现断崖式下跌).
实现在Redis单机环境下分布式锁案例.
参考:
- 基于Redis的分布式锁到底安全吗(上)?
- 基于Redis的分布式锁到底安全吗(下)?
- Distributed locks with Redis
- Redisson