源代码已上传至github: https://github.com/qiaomengnan16/spring-boot-rest-redis-lock
,检出记得修改redis连接地址。
-
应用场景:例如出现用户领券、抢红包这种高并发的情况下,用户只能抢一次,这时候简单的代码if判断在毫秒级别内无法完全控制住,数据库可能又无法做唯一锁、乐观锁等,这时候可以通过redis来控制。
-
说下思路
1. 通过使用redis的setNx命令来做同一时间内唯一并发基础。
2. 在接口层面加上锁,这时候考虑采用AOP,进入接口前加锁,结束后释放锁
3. 如果用户没有获取到锁,则直接退出
4. redis key可以考虑利用用户ID、证件号等等唯一标识
上代码
上锁解锁AOP
/**
* @Fields : redisBiz
* @author qiaomengnan
*/
@Autowired
private RedisBiz redisBiz;
@Around("execution(* com.demo..*.*(..)) && @annotation(concurrentLock)")
public Object around(ProceedingJoinPoint joinPoint, ConcurrentLock concurrentLock) throws Throwable {
String key = buildKey(concurrentLock);
try {
// 在执行前上锁,以用户名或者ID为单位作为锁
if(redisBiz.lockBySecondsTime(key, concurrentLock.time())) {
return joinPoint.proceed();
} else {
// 没有获取到锁,则把key赋值为null,以免finally里解锁
key = null;
throw new ServiceException(concurrentLock.message());
}
} finally {
// 执行完毕后解锁
if(key != null) {
redisBiz.delete(key);
}
}
}
/**
* @Title:
* @Description: 构建redis锁 key
* @return
* @throws
* @author qiaomengnan
* @date 2020/7/2
*/
private String buildKey(ConcurrentLock concurrentLock) {
// 自行根据系统业务构建出唯一的key,例如 lock_用户id , return lock_用户id
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return concurrentLock.key() + request.getParameter("userId");
}
接口层代码
@RestController
@RequestMapping("/")
public class HelloRest {
@ConcurrentLock
@GetMapping("/lock")
public String participate(String userId) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello" + userId;
}
}
接口故意休眠了10秒,这样10秒内只有这个线程拿到了锁,在做处理,其他接口请求时就会告知用户请求频繁,需要重试,直到上次操作结束。
userId为1的用户第一次访问成功了。
再访问失败了,因为这个锁被上一个线程拿去了还没有处理完。
总结:通过采用Redis的setNx实现唯一性、分布式锁,可以使用Jmeter、loadruner等并发测试工具尝试极端情况秒级、毫秒级、微妙级下的验证,AOP是辅助功能,让我们不在业务代码里加上任何锁相关的代码,只需要在接口上添加一个注解即可,这里锁默认时间是120秒,也就是说这个接口要在120秒内处理完成,如果120秒后还在继续处理,这时候锁自动失效了,则会出现问题,使用者要根据接口情况适当调整锁时间,加上过期时间是为了避免发生死锁,笔者推荐了解Redisson
这个框架。