# 用四种锁实现加入队伍功能(伙伴匹配系统)
作者:长风,编程导航星球 (opens new window) 编号 26376
# 一、为什么要对加入队伍功能上锁?
首先,要知道我们在Controller中写的代码默认是线程不安全的。在SpringBoot应用程序中,Controller实例默认都是单例对象。每当收到一个Http请求时,容器会通过反射机制调用相应的Controller方法。由于Controller是单例的,多个请求将同时访问一个Controller实例。
同一个用户的多次点击加入队伍按钮,或者不同用户同一时间点击加入队伍按钮,都会导致多个线程在访问Controller中的代码,然后就可能会出现线程不安全的问题。
例如:
伪代码:
1.从mysql中读取队伍的人数
2.判断一个队伍是否满员?
3.不满员就插入数据,满员就返回加入队伍失败
假如现在队伍人数是4/5,当前有两个用户同时点击加入队伍按钮。两个用户的请求同时到代码2发现没有满员,然后同时执行代码3将用户数据插入队伍中。就会出现队伍人数是6/5的情况。所以就需要上锁。
# 二、上锁实现方式1:单机锁-让所有用户的所有请求共用一个锁
# 2.1实现方式:
使用synchronized (this)添加同步锁,这里的this就是通过@Service创建的TeamServiceImpl对象,是一个单例对象。保证了所有用户的请求都是同一把锁。
# 2.2应用场景:
比如功能要求是判断一个队伍是否满员,满员不允许加入队伍,这种功能场景需要所有用户同一把锁。就可以使用。
# 2.3实现代码:
(只显示部分代码,附录有完整代码):
synchronized (this) {
// 判断队伍是否已满
// 从MySQL中读取队伍中人数
long teamUserNums = this.countTeamUserByTeamId(teamId);
// 判断是否满员
if (teamUserNums >= team.getMaxNum()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
}
UserTeam userTeam = new UserTeam();
userTeam.setTeamId(teamId);
userTeam.setUserId(userId);
userTeam.setJoinTime(new Date());
// 插入数据
return userTeamService.save(userTeam);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.4测试方式:
写完代码了,怎么测试一下是否有效果呢,可以在插入数据之前加入一个线程睡眠Thread.sleep(5000),保证两次请求可以同时执行插入数据代码。然后在本地可以用两个浏览器模拟两个用户同时点击的情况,同时点击加入队伍按钮。与原始代码对比。发现原始代码中会出现超员的情况,而现在的代码不会出现超员的情况。
ps:为什么要使用两个浏览器模拟两个用户?用一个浏览器新建两个窗口不行吗?
伙伴匹配系统是基于session保存用户信息的,目前我做实验得知,一个浏览器对应一个sessionId,即对应一个用户。在一个浏览器中建立多个窗口都都对应的是同一个sessionId。
# 三、上锁实现方式2:单机锁-同一个用户的所有请求共用一个锁
# 3.1实现方式:
这里需要保证每一个用户的请求是共用一把锁,就不能再用synchronized(this)了。应该
用一个每一个用户独有的一个信息去做一把锁。这里我使用的是synchronized (userId.toString().intern())
疑问1:为什么使用这个就可以解决一个用户一把锁问题
首先使用userId以区分每个用户,然后使用intern可以取出字符串常量池中的字符串对象地址,比如userId是1,每次的都是字符串常量池中“1”这个字符串对象的地址。众所周知,字符串常量池中的元素是唯一的,保证了同一Id用户的每次请求的锁对象都是相同的。
疑问2:为什么不使用synchronized (userId)
因为每次获取的userId对象地址都不同。这里每次都会new一个新的Long对象。
疑问3:为什么不使用synchronized (userId.toString())
to.String()方法会new一个String而不是直接从常量池中取出引用地址,虽然每次userId都相同,但是toString方法获取的字符串对象引用都不同。
# 3.2应用场景:
比如功能要求是判断一个用户最多加入了几个队伍或者判断一个用户是否有重复加入该队伍,这种功能场景当前用户的信息不会与其他用户的信息相互影响。所以就可以一个用户一把锁。
# 3.3实现代码:
Long userId = loginUser.getId();
synchronized (userId.toString().intern()) {
// 判断加入队伍数量是否已超出5个
LambdaQueryWrapper<UserTeam> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserTeam::getUserId, userId);
long hasJoinNum = userTeamService.count(wrapper);
if (hasJoinNum > 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入5个队伍");
}
// 插入数据
return userTeamService.save(userTeam);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 四、上锁实现方式3:分布式锁-多机部署环境下所有用户的所有请求共用一个锁
# 4.1实现方式:
使用Redis实现分布式锁。可以使用Redisson组件实现分布式锁。
# 4.2应用场景:
1.与加锁方式一相比,主要体现在分布式场景下使用。
2.所有用户的请求共用一把锁
# 4.3实现代码:
RLock lock = redissonClient.getLock("yupao:team:joinTeam");
try {
while (true) {
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
System.out.println("getLock: " + Thread.currentThread().getId());
// 判断队伍是否已满
long teamUserNums = this.countTeamUserByTeamId(teamId);
if (teamUserNums >= team.getMaxNum()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
}
UserTeam userTeam = new UserTeam();
userTeam.setTeamId(teamId);
userTeam.setUserId(userId);
userTeam.setJoinTime(new Date());
return userTeamService.save(userTeam);
}
}
} catch (InterruptedException e) {
log.error("The lock 'yupao:team:joinTeam' had a error ", e);
return false;
} finally {
// 释放锁,只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock" + Thread.currentThread().getId());
lock.unlock();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 五、上锁实现方式4:分布式锁-多机部署环境下同一个用户的所有请求共用一个锁
# 5.1实现方式:
1.使用Redis实现分布式锁。可以使用Redisson组件实现分布式锁。
2.在redis中存储的锁的key加上userId。
# 5.2应用场景:
1.与加锁方式二相比,主要体现在分布式场景下使用。
2.一个用户一把锁
# 5.3实现代码:
Long userId = loginUser.getId();
RLock lock = redissonClient.getLock("yupao:team:joinTeam:userId");
try {
while (true) {
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
System.out.println("getLock: " + Thread.currentThread().getId());
// 判断加入队伍数量是否已超出5个
LambdaQueryWrapper<UserTeam> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserTeam::getUserId, userId);
long hasJoinNum = userTeamService.count(wrapper);
if (hasJoinNum > 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入5个队伍");
}
// 插入数据
return userTeamService.save(userTeam);
}
}
} catch (InterruptedException e) {
log.error("The lock 'yupao:team:joinTeam' had a error ",e);
return false;
} finally {
// 释放锁,只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock"+Thread.currentThread().getId());
lock.unlock();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 六、待完善
1.方式1和方式2的单机锁虽然解决了线程不安全问题,但是也引入了一个新问题就是如果第一个拿到锁的任务出现了阻塞情况,会导致整个功能卡主之后的请求都没有相应。对锁可以添加一个使用时间,一定时间之后自动释放锁,防止出现拿到锁的线程出现阻塞的情况。
# 七、附录:完整代码
# 7.1单机锁-让所有用户的所有请求共用一个锁
public boolean joinTeam(TeamJoinRequest teamJoinRequest, User loginUser) {
if (teamJoinRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
if (loginUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN);
}
Long teamId = teamJoinRequest.getTeamId();
Team team = this.getById(teamId);
long userId = loginUser.getId();
// 判断加入的队伍是否过期
Date expireTime = team.getExpireTime();
if (expireTime != null && expireTime.before(new Date())) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "队伍已过期");
}
// 判断是否是私有队伍
Integer status = team.getStatus();
TeamStatusEnum teamStatusEnum = TeamStatusEnum.getEnumByValue(status);
if (TeamStatusEnum.PRIVATE.equals(teamStatusEnum)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
}
String password = teamJoinRequest.getPassword();
if (TeamStatusEnum.SECRET.equals(teamStatusEnum)) {
if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
}
}
synchronized (this) {
// 每个人最多参加5个队伍
LambdaQueryWrapper<UserTeam> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserTeam::getUserId, userId);
long hasJoinNum = userTeamService.count(wrapper);
if (hasJoinNum > 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入5个队伍");
}
// 不能重复入队
wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserTeam::getUserId, userId).eq(UserTeam::getTeamId, teamId);
long hasUserJoinTeam = userTeamService.count(wrapper);
if (hasUserJoinTeam > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
}
// 队伍是否已满
long teamUserNums = this.countTeamUserByTeamId(teamId);
if (teamUserNums >= team.getMaxNum()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
}
UserTeam userTeam = new UserTeam();
userTeam.setTeamId(teamId);
userTeam.setUserId(userId);
userTeam.setJoinTime(new Date());
return userTeamService.save(userTeam);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 7.2分布式锁-多机部署环境下所有用户的所有请求共用一个锁
public boolean joinTeam(TeamJoinRequest teamJoinRequest, User loginUser) {
if (teamJoinRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
if (loginUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN);
}
Long teamId = teamJoinRequest.getTeamId();
Team team = this.getById(teamId);
// 判断加入的队伍是否过期
Date expireTime = team.getExpireTime();
if (expireTime != null && expireTime.before(new Date())) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "队伍已过期");
}
// 判断是否是私有队伍
Integer status = team.getStatus();
TeamStatusEnum teamStatusEnum = TeamStatusEnum.getEnumByValue(status);
if (TeamStatusEnum.PRIVATE.equals(teamStatusEnum)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
}
String password = teamJoinRequest.getPassword();
if (TeamStatusEnum.SECRET.equals(teamStatusEnum)) {
if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
}
}
long userId = loginUser.getId();
RLock lock = redissonClient.getLock("yupao:team:joinTeam");
try {
while (true) {
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
System.out.println("getLock: " + Thread.currentThread().getId());
// 每个人最多参加5个队伍
LambdaQueryWrapper<UserTeam> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserTeam::getUserId, userId);
long hasJoinNum = userTeamService.count(wrapper);
if (hasJoinNum > 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入5个队伍");
}
// 不能重复入队
wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserTeam::getUserId, userId).eq(UserTeam::getTeamId, teamId);
long hasUserJoinTeam = userTeamService.count(wrapper);
if (hasUserJoinTeam > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
}
// 队伍是否已满
long teamUserNums = this.countTeamUserByTeamId(teamId);
if (teamUserNums >= team.getMaxNum()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
}
UserTeam userTeam = new UserTeam();
userTeam.setTeamId(teamId);
userTeam.setUserId(userId);
userTeam.setJoinTime(new Date());
return userTeamService.save(userTeam);
}
}
} catch (InterruptedException e) {
log.error("The lock 'yupao:team:joinTeam' had a error ", e);
return false;
} finally {
// 释放锁,只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock" + Thread.currentThread().getId());
lock.unlock();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83