8

引言

本文只是对分布式锁的一个简单的理论入门,不够完善,如果您想学习完整、系统的分布式锁,还请莫在本文浪费时间。

原计划使用Redis作为系统缓存提升系统性能,在探究过程中发现序列化过于耗时,在编辑到一半时将原计划作废。

原计划

本地测试的时候,构造了大量数据,但MySQL表现却并不理想。

测试数据:262144条,单表查询。如果平均到每个教师的出题数来说,二十万很正常。

image.png

分页查询接口,每页十条,查询第一页数据,竟然花费了惊人的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应该关联出来了许多数据所以才需要这么长的时间去序列化。

image.png

整到这直接放弃,这个优化方案不合适。原计划夭折。

分布式锁

既然挂在序列化上了,也不能否认Redis的高性能,就讲讲面试常考的Redis分布式锁的理论吧,具体实现去找开源项目一大堆。

分布式锁听起来挺高大上,其实特别简单。

image.png

在集群环境下,多个后台服务去操作数据库,如果并发操作,某些场景下会产生问题。

假设数据表如下,又是余额的例子:

id balance
1 500

余额500

这是并发都会遇到的问题,下面进行统一描述。

两个任务:AB并发执行,AB可以是一台服务器上的两个线程,也可以是集群下的不同服务器中的线程。

A执行取钱,取200

B执行存钱,存200

AB查询原余额,取到的都是500,并发执行完加减操作后。

A500 - 200 = 300

B500 + 200 = 700

所以AB将结果写回数据库时,无论谁先谁后,最终的结果都不正确。

所以需要通过加锁的方式来解决,A执行时,加锁,B再执行时,尝试获取锁失败,等待,A执行完成,解锁,B获取到锁,执行,解锁。反之亦然。

单机环境下的加锁方案请参照美团博客:不可不说的Java“锁”事 - 美团技术团队,面试必考,不会不行,学完使不上,两天就得忘。

集群环境下的并发问题,就需要分布式锁了。

因为集群环境下,各服务实例互不影响,不共享内存,传统的像ReentrantLock之类的普通加锁方式在分布式环境下无效。

三者通过共同的Redis实例实现加解锁。

image.png

大致流程如下:

ARedis中写入“A正在执行用户1的取钱操作,请其他操作用户1的任务等待”。

B看到了Redis中的数据,等待稍候重试,或直接结束使操作失败。

A执行,执行完成,清除在Redis中写入的数据。

如果B稍候重新执行,去Redis中查询,没有查询到互相冲突的实例在执行的消息,开始执行。

用专业的话来讲,这就是分布式锁。

专业描述

ABRedis中执行SETNX命令。

SETNXSET 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是单线程的,所以假设AB同时去Redis中加锁,因为Redis单线程,必然有先后顺序,不会出现AB同时加锁成功的情况。

以上只是理论,也是最基本最古老的分布式锁实现原理,同样存在诸多问题,Redis宕机了怎么办?解锁失败的时候怎么办?

真不知道去年的我们是怎么学会这些东西的。

image.png

总结

吾生也有涯,而知也无涯。


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。