Skip to content

使用java的多线程与redis的pipeline、watch、mutil等命令模拟学生并发争抢宿舍床位的过程

License

Notifications You must be signed in to change notification settings

goldsudo/choose-room

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

使用redis实现并发锁,解决高并发下的学生争抢宿舍床位从而引起的并发问题

项目介绍

本程序使用java的多线程与redis的pipeline、watch、mutil等命令模拟学生并发争抢宿舍床位的过程。
具体的模拟场景为8个学生同时争抢4个床位,最终要求4个床位有且只能有一个学生入住。
本程序首先向redis中初始化4个床位,然后创建8个学生,每个学生都有一个目标床位,然后创建8个线程来并发执行每个学生的床位选择,以此模拟实际业务中的并发场景。

模拟结果(每次结果随机)

床位初始化成功,一共4个空床位:BED_1 BED_2 BED_3 BED_4
STUDENT_8进行选房-------目标床位为 BED_1
STUDENT_7进行选房-------目标床位为 BED_2
STUDENT_6进行选房-------目标床位为 BED_3
STUDENT_5进行选房-------目标床位为 BED_4
STUDENT_4进行选房-------目标床位为 BED_4
STUDENT_3进行选房-------目标床位为 BED_3
STUDENT_2进行选房-------目标床位为 BED_2
STUDENT_1进行选房-------目标床位为 BED_1
STUDENT_2进行选房-------获取到床位锁
STUDENT_1进行选房-------获取到床位锁
STUDENT_5进行选房-------没有获取到床位锁,该床位已有学生正在选择
STUDENT_7进行选房-------没有获取到床位锁,该床位已有学生正在选择
STUDENT_6进行选房-------获取到床位锁
STUDENT_4进行选房-------获取到床位锁
STUDENT_8进行选房-------没有获取到床位锁,该床位已有学生正在选择
STUDENT_3进行选房-------没有获取到床位锁,该床位已有学生正在选择
STUDENT_2进行选房-------选房成功
STUDENT_1进行选房-------选房成功
STUDENT_4进行选房-------选房成功
STUDENT_6进行选房-------选房成功
模拟选房执行完毕
-----本次模拟选房结果-----
床位:BED_1 入住学生:STUDENT_1
床位:BED_2 入住学生:STUDENT_2
床位:BED_3 入住学生:STUDENT_6
床位:BED_4 入住学生:STUDENT_4

业务背景

之前在原来的公司做过新生入学选房系统,大致的功能就是将所有的新生宿舍放到系统上,定时开放选房,然后学生们可以自己选择想住的宿舍和床位。

并发问题

既然是学生自行选择,那么一些抢手的宿舍就会有很多学生都去选,比如离食堂近,离运动场进,或者设备更齐全的宿舍。
而且由于选房是定时开放的,比如具体到某一天的下午2点整开启,那么在2点整的那一刻,热门宿舍的并发量可能会很高。
此时就引来了并发问题了,数据库只有一个,但是选房系统是部署在多台服务器上的,因此要在集群环境下避免出现同一个床位住了2个人这样的“乌龙”事件发生。

解决方案

第一个版本——没有锁

那会选房系统刚刚做出来第一版,由于项目时间紧促,当时的开发人员没有去重视并发问题,仅仅是在入库选房结果前执行了一次数据库查询保证数据的正确性。<br 存在的问题:
这个版本的方法问题很明显,当同时有两个学生选择同一张床位时,可能两个学生在入库选房结果前,都查到该床位当前没有人住,然后都入库了自己的选房结果,导致一张床被两个人重复入住。

第二个版本——1s失效时间的宿舍锁

第一个版本果然在学校现场出现了问题,于是进行紧急修复,当时是我们部门的技术leader利用redis(选房系统本身就用了redis缓存了业务数据进行查询的提速)实现了一个简单的分布式锁。
在进行选房操作之前,先执行redis的setnx指令,将当前宿舍的id作为key值(失效时间为1s),如果返回结果为1,代表获取到了宿舍锁,可以进行选房,此时其他学生再选择这个宿舍中的其他床位时,执行setnx 宿舍id 的结果将为0,此时就提示学生“宿舍目前很抢手,请稍后再试”。
存在的问题:
这个版本的解决方案是利用了redis来实现分布式锁,确实能够解决问题。但也存在两个问题。
第一个问题是影响了并发量,由于上锁的是宿舍而不是床位,这导致一旦一个学生去选择某个宿舍的某个床位,同一时间所有选择这个宿舍的学生都无法进行选房,不管床位是不是和这个学生选择的床位一致。
这就导致本来1s之内,一个宿舍(4个床位)是最多可同时让4个人进行选房的,但是现在只允许1个学生进行选房了。其他学生需要等1s之后才能再次选择该宿舍的床位。
而第二个问题则是1s这个失效时间的不确定性导致,因为选房操作包含着一系列的数据库读取写入操作,还包括缓存的更新操作,这些操作如果无法在1s的时间内完成的话,就有可能出现问题,因为1s过后redis已经删除掉宿舍锁了,其他学生又可以选择该宿舍的床位了。

第三个版本——5s失效时间的自释放锁

我是在第二个版本之后接手选房系统的,当时读了一遍代码后发现了上述的问题,于是给出了一个优化策略来解决1s失效时间的不确定性问题。
具体的操作是将宿舍锁的失效时间延长至5s,并且在执行完选房操作后,主动去释放该宿舍锁。
5s的时间足够保证执行选房的那一系列操作的执行完成了,并且在执行结束后让线程主动去释放锁,不会耽误后续的操作,这样在原来的基础上保证了安全性。

第四个版本——降低宿舍锁的颗粒度为床位锁

之所以第二个版本中使用的锁是宿舍锁而不是床位锁,是因为当时的情况紧急,现场除了问题,需要给出一个快速的解决方案,所以没有去考虑如何实现更复杂的床位锁。
我在进行第三个版本的优化后,又进行了一些思考,最后将宿舍锁颗粒度降为床位锁。
过程中遇到了一些难题,首先选房系统是支持组队选房的,只要队内的任何一个学生选到了足够的床位,那么队员也能成功入住。
这就意味着如果有一个2人队,学生1选择了某宿舍的1、2号床,学生2同时选择了该宿舍的3、4号床,由于两人都拿到了床位锁,所以都选房成功了,结果就是2个人住了4张床。
还有一个就是如何原子的获取多个床位锁的问题,因为组队选房存在一次性选择多个床位的情况,所以在上床位锁时,要么全部锁成功,要么全部锁失败,否则出现占用锁的问题。
关于组队选房的问题,我设计一个叫团队锁的方案来解决,在学生尝试获取床位锁之前,先尝试获取团队锁,这样能保证同一时间同一个团队只能一个人进行选房。
关于上锁操作原子性的问题,我使用了reids的pipeline来开启一个管道,然后watch将要上锁的所有床位key,开启一个multi事务,在事务内进行所有床位key的setnx操作,最后由pipeline统一提交。
这样的话,如果在当前学生尝试上锁期间,有别的学生已经成功锁注当前学生选择的部分或所有床位,那么当前学生的上锁事务就会失败,就不会出现部分床位上锁成功的问题了。

About

使用java的多线程与redis的pipeline、watch、mutil等命令模拟学生并发争抢宿舍床位的过程

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages