分布式锁(防止同时操作同一条数据)实现分析
1. deleteLocked 方法:
public R deleteLocked(String id, String username) {String examReportUserKey = "examReportId_" + id + "_" + username;stringRedisTemplate.delete(examReportUserKey);return R.ok();
}
功能:删除指定用户对某个 ExamReport 的锁。
实现:通过删除 Redis 中对应的键来释放锁。
2. checkIfLocked 方法:
public boolean checkIfLocked(Long id, String username) {String examReportKey = "examReportId_" + id + "_";String examReportUserKey = "examReportId_" + id + "_" + username;Set keys = stringRedisTemplate.keys(examReportKey + "*");boolean iflocked = true;if (keys == null || keys.size() == 0) {iflocked = false;}for (Object key : keys) {if (examReportUserKey.equals(key.toString())) {String lockflag = stringRedisTemplate.opsForValue().get(examReportUserKey).toString();if (lockflag.equals("1")) {iflocked = false;break;}}}return iflocked;
}
- 功能:检查某个 ExamReport 是否被其他用户锁定。
- 实现:
获取所有以 examReportKey 开头的键。
检查是否存在与当前用户相关的键,并且该键的值为 "1"。
如果存在且值为 "1",则表示未被锁定;否则表示已被锁定。
3. getLockIds 方法:
public String getLockIds(String username) {String examReportKey = "examReportId_";Set keys = stringRedisTemplate.keys(examReportKey + "*");String lockIds = "";List<String> lockIdsList = new ArrayList<>();for (Object key : keys) {String ids = key.toString();String[] parts = ids.split("_");if (!parts[2].equals(username)) {if (!lockIdsList.contains(parts[1])) {lockIdsList.add(parts[1]);}}}lockIds = String.join(",", lockIdsList);return lockIds;
}
- 功能:获取与特定用户名不匹配的锁 ID 列表。
- 实现:
获取所有以 examReportKey 开头的键。
提取与特定用户名不匹配的锁 ID,并去重。
4. checkUserLocked 方法
public void checkUserLocked(Long id, String username) {String examReportUserKey = "examReportId_" + id + "_" + username;stringRedisTemplate.opsForValue().set(examReportUserKey, "1", 1, TimeUnit.HOURS);
}
- 功能:为指定用户对某个 ExamReport 设置锁。
- 实现:
在 Redis 中设置一个键,键名为 examReportUserKey,值为 "1",过期时间为 1 小时。
5. examReportService.lambdaQuery 方法
ExamReport examReport = examReportService.lambdaQuery().eq(ExamReport::getDelFlag, "0").eq(ExamReport::getStatus, 1).eq(ExamReport::getId, id).notIn(StringUtils.hasLength(ids), ExamReport::getId, ids).last("limit 1").one();
- 功能:查询符合条件的 ExamReport 记录,并排除已经获取到的锁 ID。
- 实现:
eq(ExamReport::getDelFlag, "0"):查询 del_flag 为 "0" 的记录。
eq(ExamReport::getStatus, 1):查询 status 为 1 的记录。
eq(ExamReport::getId, id):查询 id 为指定 id 的记录。
notIn(StringUtils.hasLength(ids), ExamReport::getId, ids):如果 ids 不为空,则排除 id 在 ids 列表中的记录。
last("limit 1"):在查询的最后添加 LIMIT 1,确保只返回一条记录。
one():执行查询并返回一条记录。
6. queryVerifyList 方法
public R queryVerifyList(@Param("prevId") String prevId) {String username = SecurityUtils.getUser().getUsername();String ids = getLockIds(username);RestTemplate restTemplate = new RestTemplate();Map<String, Object> map = new HashMap<>();map.put("ids", ids);map.put("prevId", prevId);String url = basePath + "/appapi/queryVList?ids={ids}&prevId={prevId}";R result = restTemplate.getForObject(url, R.class, map);if (result == null) {return R.failed("请求失败");}if (result.getCode() == 200) {ExamReportCheckVO examReportCheckVO = JSON.parseObject(JSON.toJSONString(result.getData()), ExamReportCheckVO.class);checkUserLocked(examReportCheckVO.getId(), username);deleteLocked(prevId, username);return R.ok(examReportCheckVO);}return result;
}
- 功能:查询验证列表,并处理锁的获取和释放。
- 实现:
获取当前用户的用户名。
调用 getLockIds 方法获取当前用户未锁定的 ExamReport ID 列表。
使用 RestTemplate 发送 HTTP GET 请求到指定 URL,传递 ids 和 prevId 参数。
处理请求结果:
如果请求失败,返回失败响应。
如果请求成功且状态码为 200,解析返回的数据为 ExamReportCheckVO 对象。
调用 checkUserLocked 方法为新的 ExamReport 设置锁。
调用 deleteLocked 方法释放旧的 ExamReport 锁。
返回成功响应,包含 ExamReportCheckVO 对象。
如果请求成功但状态码不是 200,直接返回请求结果。
分布式锁的实现原理
1. 锁的获取
checkUserLocked 方法:
为指定用户对某个 ExamReport 设置锁。
键的格式为 examReportId_{id}_{username},值为 "1",过期时间为 1 小时。
2. 锁的检查
checkIfLocked 方法:
检查某个 ExamReport 是否被其他用户锁定。
通过查询 Redis 中的键来判断是否存在与当前用户相关的锁,并且该锁的值为 "1"。
如果存在且值为 "1",则表示未被锁定;否则表示已被锁定。
3. 锁的释放
deleteLocked 方法:
删除指定用户对某个 ExamReport 的锁。
通过删除 Redis 中对应的键来释放锁。
4. 查询记录
examReportService.lambdaQuery 方法:
查询符合条件的 ExamReport 记录,并排除已经获取到的锁 ID。
确保返回的记录是未被锁定的。
5. 查询验证列表
queryVerifyList 方法:
获取当前用户的用户名。
获取当前用户未锁定的 ExamReport ID 列表。
发送 HTTP 请求查询验证列表。
处理请求结果,设置新锁并释放旧锁。
优缺点
优点
简单易懂:实现逻辑简单,容易理解和维护。
高可用性:利用 Redis 的高可用性和持久化特性,确保锁的可靠性和一致性。
自动过期:设置锁的过期时间,避免死锁问题。
缺点
性能问题:频繁的 keys 操作可能会导致性能瓶颈,特别是在数据量较大的情况下。
竞态条件:在高并发环境下,可能存在竞态条件,导致锁的不一致。
依赖 Redis:整个锁机制依赖于 Redis,如果 Redis 出现故障,锁机制将失效。
示例
假设 Redis 中有以下键:
examReportId_123_user1
examReportId_456_user1
examReportId_789_user2
examReportId_101_user3
操作流程
获取锁 ID 列表:
调用 getLockIds("user1") 会返回 789,101。
查询记录:
使用 examReportService.lambdaQuery 方法查询符合条件的 ExamReport 记录,并排除 789 和 101 这些已经被锁定的 ID。
检查锁状态:
调用 checkIfLocked(123, "user1") 会返回 false,表示 123 已经被 user1 锁定。
释放锁:
调用 deleteLocked("123", "user1") 会删除 examReportId_123_user1 键,释放锁。
查询验证列表:
调用 queryVerifyList("123") 会发送 HTTP 请求查询验证列表,并处理锁的获取和释放。
总结
通过上述方法,实现了一个基于 Redis 的分布式锁机制,确保在同一时间只有一个用户可以访问某个资源,从而避免并发冲突。虽然存在一些性能和竞态条件的问题,但在大多数场景下,这种实现方式是有效且可靠的。