引言
本文只是对分布式锁的一个简单的理论入门,不够完善,如果您想学习完整、系统的分布式锁,还请莫在本文浪费时间。
原计划使用Redis
作为系统缓存提升系统性能,在探究过程中发现序列化过于耗时,在编辑到一半时将原计划作废。
原计划
本地测试的时候,构造了大量数据,但MySQL
表现却并不理想。
测试数据:262144
条,单表查询。如果平均到每个教师的出题数来说,二十万很正常。
分页查询接口,每页十条,查询第一页数据,竟然花费了惊人的12.09
秒,太慢了。
决定使用Redis
缓存进行查询优化。
引入Spring Data Redis
,安装Redis
,配置Redis
,配置Serializable
接口,配置Cacheable
注解。
@Cacheable(value = "pageAllByCurrentUser")
public Page<Subject> pageAllByCurrentUser(Pageable pageable, Long courseId, Long modelId, Integer difficult) {
}
测试,首次请求花费了18.80
秒,也就是说序列化10
条数据花费了6
秒,推测Hibernate
应该关联出来了许多数据所以才需要这么长的时间去序列化。
整到这直接放弃,这个优化方案不合适。原计划夭折。
分布式锁
既然挂在序列化上了,也不能否认Redis
的高性能,就讲讲面试常考的Redis
分布式锁的理论吧,具体实现去找开源项目一大堆。
分布式锁听起来挺高大上,其实特别简单。
在集群环境下,多个后台服务去操作数据库,如果并发操作,某些场景下会产生问题。
假设数据表如下,又是余额的例子:
id | balance |
---|---|
1 | 500 |
余额500
。
这是并发都会遇到的问题,下面进行统一描述。
两个任务:A
和B
并发执行,AB
可以是一台服务器上的两个线程,也可以是集群下的不同服务器中的线程。
A
执行取钱,取200
。
B
执行存钱,存200
。
A
、B
查询原余额,取到的都是500
,并发执行完加减操作后。
A
:500 - 200 = 300
B
:500 + 200 = 700
所以A
、B
将结果写回数据库时,无论谁先谁后,最终的结果都不正确。
所以需要通过加锁的方式来解决,A
执行时,加锁,B
再执行时,尝试获取锁失败,等待,A
执行完成,解锁,B
获取到锁,执行,解锁。反之亦然。
单机环境下的加锁方案请参照美团博客:不可不说的Java“锁”事 - 美团技术团队,面试必考,不会不行,学完使不上,两天就得忘。
集群环境下的并发问题,就需要分布式锁了。
因为集群环境下,各服务实例互不影响,不共享内存,传统的像ReentrantLock
之类的普通加锁方式在分布式环境下无效。
三者通过共同的Redis
实例实现加解锁。
大致流程如下:
A
在Redis
中写入“A
正在执行用户1
的取钱操作,请其他操作用户1
的任务等待”。
B
看到了Redis
中的数据,等待稍候重试,或直接结束使操作失败。
A
执行,执行完成,清除在Redis
中写入的数据。
如果B
稍候重新执行,去Redis
中查询,没有查询到互相冲突的实例在执行的消息,开始执行。
用专业的话来讲,这就是分布式锁。
专业描述
A
、B
在Redis
中执行SETNX
命令。
SETNX
:SET if Not eXist
。如果key
不存在,进行SET
,否则失败。
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
所以,谁在Redis
中执行SETNX
成功,就相当于那个服务实例加锁成功。
加锁成功的继续执行,失败的等待,稍后重试。
执行完成后,将Redis
中的key
删除,其他服务实例即可重新获取锁。
为什么 Redis 不会产生并发问题?
因为Redis
是单线程的,所以假设A
和B
同时去Redis
中加锁,因为Redis
单线程,必然有先后顺序,不会出现A
与B
同时加锁成功的情况。
以上只是理论,也是最基本最古老的分布式锁实现原理,同样存在诸多问题,Redis
宕机了怎么办?解锁失败的时候怎么办?
真不知道去年的我们是怎么学会这些东西的。
总结
吾生也有涯,而知也无涯。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。