项目-01-Redis单机情况分布式锁实现案例-功能描述

image-20210430130246755

  • 项目地址: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 安装在本虚拟机中.

image-20210430130246755

项目描述:

问题复现:

以用户购买 商品编号为goods:01(默认为100个) 商品 为例,

在不加锁的情况下会出现重复购买同一商品现象, 在加锁的情况下, 可以解决重买问题,但是因为加了锁,会导致性能下降.

单机情况:

不加锁:

代码实现:
  1. GoodsController.class
  2. GoodsService.class
  • 情景: 100个用户 ,不加锁, 购买商品编号为goods:01 的商品,

image-20210518123227938

http://localhost:1111/buy_goods/goods:01

结果:

出现重买(一件商品卖给多个人)现象, 按理说的话,100 间商品,100人购买,商品最后的数量是0才对.

image-20210518123650708

Redis数据库数据如图所示, 100人购买同一商品,商品出现了剩余,说明购买过程中出现了重复购买的情况. 即上图 所示.

image-20210518123418484

吞吐量: 97.6/s

image-20210518124142069

加锁:

代码实现:
  • synchronized: GoodsController.class
  • ReentrantLock: GoodsController.class

请求地址: http://localhost:1111/sync/buy_goods/goods:01

  • 情景再现: 100 个用户, 购买goods:01 商品
  • 测试案例按照Synchronized实现测试
结果:

Redis数据库查询结果为:

image-20210518124257541

后台打印输出为:

image-20210518124340391

吞吐量: 87.6/s

image-20210518124435975

结论:

加了synchronized 锁以后, 解决了重买问题,但是系统吞吐量从97/s 降到了87/s, 说明系统性能确实有所下降.

分布式情况:

修改 windows 系统的Hosts文件,虚拟机Ip地址映射成 mycentos.com , 以下访问内容基于此实现.

系统架构:

image-20210430130246755

不加锁:

http 请求: http://mycentos.com/sync/buy_goods/goods:01

  • 情景: 100 个用户, 购买商品编号goods:01 的商品(商品有100 个)
结果:

image-20210531180412091

结果1 : 商品 goods:01 并没有卖完

image-20210531180135682

image-20210531180316148

由两图可知, 系统一共打印了100 条信息(说明不存在请求超时的请求–即每一个请求都成功获得了锁), 然后商品剩余11件, 加上图中红框标记内容,说明 商品存在超卖.

结果2: 商品出现超卖现象

结论:

Synchronized 可以解决单机情况下商品超卖问题,但不可以解决分布式情况下商品超卖问题, 因此,对于分布式应用,若要保证商品购买不出现超卖现象, 需要加分布式锁.

加锁:

如何加锁?
解决方案:

根据不加分布式锁出现商品超卖现象, 笔者决定给项目添加加分布式锁(利用redis 固定一个对象作为锁对象).

Redis 官网给了两种实现方式:

  1. 采用setnx加锁,lua 脚本解锁来实现分布式锁.

  2. 采用Redisson–Redlock 算法实现分布式锁.

原子操作SetNx 实现分布式锁
实现依据:

Redis单机情况下实现分布式锁

image-20210531230521711

http://mycentos.com/redis/setnx/buy_goods/goods:01

  • 情景一 : 100 个用户, 购买商品编号goods:01 的商品(商品有100 个)
代码实现:

RedisSetNxGoodsController.class

情景一结果:

image-20210531180925997

image-20210531181012038

image-20210531181048896

由图知boot-redis-1111: 一共打印了40 条数据, boot-redis-2222: 一共打印了47调数据, 然后redis中商品goods:01剩余13件, 40+47+13=100,说明, 存在部分请求没有成功获取到锁,并且经校验后发现,没有商品存在超卖现象(注意,我们现在是100个请求,去买商品, 根据打印行数是可以判断商品超卖的).

出现此问题的原因:

image-20210531224742156

最开始我以为是,线程数量太少(所以选择加大线程数,然后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个), 然后判断商品是否存在超卖现象(判断方法,人眼比对).
情景二结果:

image-20210531215405561

image-20210531215456144

由上图表示说是, 49+51=100,(boot-redis-1111:49 件, boot-redis-2222:51 件), 经过比对后,没有出现商品goods:01的超卖情况.

吞吐量:100/s

image-20210531230213073

100/s

可能出现的问题:
  1. 此锁必须设置过期时间,否则若应用1获取锁后,应用1直接宕机,导致应用1一直持有这把锁,其他应用不可以获取锁了.

  2. 不同线程删除不属于他的锁该怎么办?

    解决方案: 删除前判断锁是否属于当前线程, 若属于当前线程,则可以正常删除, 若不是,不做处理.

  3. 实际场景中,在线程获取锁后,未在锁超期时间内完成事务, 如何实现锁续期问题? 锁的有效时间又该如何设计呢?

结论:

在单机Redis情况下, 利用set nx +超时时间 进行加锁, 以及用lua脚本方式进行解锁(补充: 也可以利用Redis事务的方式来实现, 不过,推荐lua脚本, 原子性), 可以实现分布式锁,但是,若线程A在规定时间内(Redis分布式锁超期时间)内未完成业务,线程A不得不放弃业务执行,显然,这是不人性的,所以引出Redisson (RedLock 算法)来解决这个问题.

Redission 实现分布式锁:
理论依据:
//RedissonLock.scheduleExpirationRenewal(threadId); 
// RedissonLock.renewExpiration(threadId);

Redisson 加锁解锁: 通过lua脚本实现.

开了一个线程, 不断续约直到 业务代码执行完成.

image-20210601124010516

代码实现:

代码实现过程中,我给出了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 个)

image-20210531235713676

结果:

image-20210531235523172

image-20210531235552198

由图知, boot-redis-1111, boot-redis-2222 分别处理了50个请求, 未出现超卖现象.

吞吐量:

image-20210601000012550

结论:

Redis 单机情况下实现分布式锁,保证锁可以自动续期 ,Redisson 给出了完美解决方案, 建议是如果业务代码执行时间可以固定,可以选择给出leaseTime(线程持有锁的最大时间),若不能,交给Redisson去实现锁续期.

但是在Redis集群环境下,Redis分布式锁会存在各种各样的问题,下面给出两篇文章可以帮助你进一步的理解分布式锁.

具体参考:

  • 基于Redis的分布式锁到底安全吗(上)?
  • 基于Redis的分布式锁到底安全吗(下)?
疑问 ?

Redisson实现的分布式锁是否整的安全?

答: 在Redis单机情况下,Redisson分布式锁是安全的, 但是在集群Redis环境中,分布式锁存在问题.

总结:

  • 吞吐量的测试: 纯粹是自己闹着玩的,别当真,当真你就输了(1轮测试中吞吐量大,多轮测试后吞吐量呈现断崖式下跌).

  • 实现在Redis单机环境下分布式锁案例.

参考:

  • 基于Redis的分布式锁到底安全吗(上)?
  • 基于Redis的分布式锁到底安全吗(下)?
  • Distributed locks with Redis
  • Redisson