—— 基于Redis 单节点
库存超卖有很多种解决方案,悲观锁,乐观锁,zookeeper 分布式锁,redis 分布式锁。这篇笔记介绍如何 redis 分布式锁。当然,redlock 实现分布式锁备受争议,就连作者也不推荐使用。Redis 官方推荐了一个 Redis Java Client —— Redisson
。
分布式锁
什么是分布式锁?
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。不同的系统或者同一个系统的不用主机之间共享同一个或同一组资源,那么访问这些资源的时候,往往需要互斥防止彼此干扰,进而保证了数据一致性。分布式系统下如果想实现线程同步,可以使用分布式锁。
分布式锁有以下特点:
- 互斥性:一个方法在同一时间段只能被一台机器的一个线程执行
- 防止死锁:具有失效机制,防止死锁
- 容错性:集群环境中,只要节点大多数正常运行,就能正常加解锁
- 解铃还须系铃人:加解锁必须是同一个客户端
- 可重入性:同一个客户端获得锁后可递归调用
- 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock(阻塞) 和 trylock(非阻塞)
- 支持公平锁和非公平锁(可选):公平锁,多个客户端同时请求加锁时,优先分配给先发出请求的线程;非公平锁,相比公平锁而言,它是无序的,不存在什么先来后到
当然,Redisson 就具有这些特点。
Redisson
Redisson
是一个具有内存数据网格的 Redis Java Client。它为使用 Redis 提供了更方便、最简单的方法。它提供了关注点分离,是你能集中精力去处理业务逻辑。Redisson
底层是采用 Netty
框架。支持 Redis 2.8
以上版本,支持 Java 1.6
以上版本。Redisson
提供了很多很强大的功能,详细可以看官网Wiki目录。其中,分布式锁就有很多种,而这里选择的是公平锁。其它的锁也可以使用,看需求,看场景,选择合适即可。:satisfied:
看门狗
摘取自官网 Wiki:大家都知道,如果负责储存这个分布式锁的 Redisson节点(Redisson Java Client)宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson 内部提供了一个监控锁的看门狗,它的作用是在 Redisson 实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
摘取自源码:锁定监视器超时(毫秒)。仅在未定义 leaseTimeout 参数的情况下获取锁时才使用此参数
。如果看门狗没有将锁延长到下一个 LockWatchDogTimeout 时间间隔,则锁将在 LockWatchDogTimeout 之后过期。这可以防止由于 Redisson 客户端崩溃或任何其他原因而导致无限锁定或者锁无法以正确的方式释放。
总结:
它的作用就是给所有未指定 leaseTimeout 参数的锁添加一个超时时间,这个时间就是 LockWatchDogTimeout 的值,默认每隔 internalLockLeaseTime / 3 秒(internalLockLeaseTime 等于 LockWatchDogTimeout,可以去看源码)检查一次,Redisson 节点正常,它就会不断延长锁的持有时间,直到 unlock;当 Redisson 节点宕机后,它不再延长,这个时候锁在持有时间用完之后就会释放锁,可以有效防止死锁。
环境/版本一览:
- 开发工具:Intellij IDEA 2018.2.2
- springboot: 2.1.1.RELEASE
- jdk:1.8.0_171
- maven:3.3.9
- redisson-spring-boot-starter:3.11.2
- docker:19.03.1
- redis:5.0.5
注:这里 SpringBoot 的版本建议与 Redisson starter
中 SpringBoot 版本保持一致
1、pom.xml
1 | <dependencies> |
2、yml
application.yml
1 | spring: |
redisson.yaml
1 | # 单redis节点模式配置文件样本 |
3、constants
1 | package com.fatal.constants; |
4、annotation
1 | package com.fatal.annotation; |
5、aspect
核心方法:
1 | /** |
书写切面:
1 | package com.fatal.aspect; |
6、service
IBusinessService.java
1 | package com.fatal.service; |
BusinessServiceImpl.java
刚开始我还在想,AtomicInteger 这种类的所有方法都具有原子性,那我之前和数据库交互的实体属性都只是包装类,那并发情况下对实体属性的操作是非线程安全的,那会不会引起线程安全问题?后来一想,考虑线程是否安全应该体现在共享数据上面,实体的属性 get 出来后是局部变量(局部嘛,多个线程的同一个方法的局部变量互不影响的),没有必要考虑线程安全与否,从实体属性的角度出发,不需要用原子类。真正需要考虑线程安全的是对数据库的操作,数据库中的数据当然是共享数据,笑了:smile:。这里的疑问应该就是概念混淆了。不过可以想象一下,数据库加事务后也具有原子性,AtomicInteger <-> 数据库 ? 我们倒可以用 AtomicInteger 来模拟对数据库的操作呢!于是接下来就偷个懒了,不建数据库,new 个 AtomicInteger 对象来做 Demo 就行。->
1 | package com.fatal.service.impl; |
7、controller
1 | package com.fatal.controller; |
8、Test
1 | package com.fatal; |
9、测试
首先,启动虚拟机。为什么选择虚拟机呢?因为我觉得虚拟机中安装 redis,是比较接近生产环境的。麻烦?你可以使用 docker 安装 redis 来测试,不会麻烦的。(这里我安装的是最新版本 5.0.5)
在测试中,我们还会使用一个工具,就是 Apach ab 来进行压测。
整合 Redisson
运行 RedissonTests.fairLockTest()
Redis 图形化界面
模拟超卖
启动程序
使用 ab 压测。来到 bin 目录下,输入以下命令:
1 | ab -n 1000 -c 1000 http://localhost:8080/withoutLock |
-n:总请求数
-c:并发数
访问 http://localhost:8080/query
你可以发现,超卖了…
解决超卖
启动程序
使用 ab 压测。来到 bin 目录下,输入以下命令:
1 | ab -n 1000 -c 1000 http://localhost:8080/withLock |
访问 http://localhost:8080/query
坑
问题
Windows 下的 Redis 和 Redisson starter 一起用会出现问题。报错如下:
1 | 2019-08-29 14:54:56.755 ERROR 16728 --- [isson-netty-2-9] o.redisson.client.handler.CommandsQueue : Exception occured. Channel: [id: 0x5b2c40f0, L:/127.0.0.1:65492 - R:localhost/127.0.0.1:6379] |
应用启动之后,压测后隔段时间就出现这些错误,应用关闭时也会出现这些错误。:sweat:
解决
刚刚又重新下载了 Windows 版本的 Redis3.2.100,只改密码,然后发现还是报错,官网也能看出来很久没维护了,我想问题应该出现在 Windows 版本的 redis 上。
建议使用 Linux 版本的。 :smirk:
笔记
压测的时候,用 Redis 图形化界面看,如下:
A:为什么使用 hash 这种结果存储锁呢?
B:如果直接 String 的话, UUID:ThreadId
就不知道是谁的锁,后面还有别的锁的话, UUID:ThreadId
可能会重复。类似命名空间的思想,选择 hash,可以避免了 key 的碰撞。
A:那不用 UUID:ThreadId
不行吗?
B:不用的话怎么实现 解铃还须系铃人,是吧。我觉得用 hash 不是为了存放多个 field,只是它这种数据结构刚好适合而已。
看下源码:
1 | ... |
Redisson 的公平锁就是凭 redisson_lock_queue:{lockName}
、redisson_lock_timeout:{lockName}
来实现的。
参考资料
…
总结
SpringBoot
的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn、方志朋的专栏、程序猿DD、纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~
以上文章是我用来学习的Demo
,都是基于 SpringBoot2.x
版本。
源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter31