项目背景:项目上有个接口,日志显示短时间内被多次调用,导致相应错误,故需要限制用户不能在短时间内多次调用接口。可以从前端或后端同时限制:前端做按钮点击后的loading效果,这里主要介绍后端接口如何通过redis锁限制。
什么是redis锁?
我们都知道redis是一个可以存储key-value的数据库,我们往redis里面存一个key,就相当于上了一把锁,当我们能在redis里面找到这个key,就相当于找到这个锁啦!同时,我们还应该知道往redis里设置key的时候,是可以同时设置过期时间,也可以手动删除这个key的。这样我们就有两种解锁的方式。
redis配置?
这里简单说明下redis大概有哪些配置:
配置文件
配置类
工具类
如何使用redis锁?
前面说到redis锁就是一个key,那么我们可以在工具类里写这两个方法设置key
/**
* set nx,上锁
* @param key 一般设为lock
*@param value 一般使用uuid
*@param time 缓存时间,单位为s
*/
public boolean setNx(String key, String value, int time){
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS));
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Long timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
业务代码:通过用户id来设置key值,这样每个用户都有一把锁,上锁成功继续执行业务代码,最后finally里面删除key,解锁。
String lock = "lock" + SecurityUtils.getUserId();
boolean isLock = redisService.setNx(lock, String.valueOf(SecurityUtils.getUserId()), 10);
if (isLock) {
try {
// 业务代码
} finally {
redisService.deleteObject(lock);
}
} else {
log.info("请勿重复访问程序:{}", lock);
}
注意点(坑):我一开始是这么写的,设置key(上锁),查询key(判断是否上锁),解锁(删除key)
String lock = "lock" + SecurityUtils.getUserId();
log.info("获取锁,key:{},value:{}", lock, redisService.getCacheObject(lock));
try {
if (Objects.isNull(redisService.getCacheObject(lock))) {
// 上锁
redisService.setCacheObject(lock, SecurityUtils.getUserId(), 10L, TimeUnit.SECONDS);
log.info("设置锁,key:{},value:{}", lock, redisService.getCacheObject(lock));
} else {
log.info("程序执行中,请勿重复访问");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
log.info("解锁,key:{},value:{}", lock, redisService.getCacheObject(lock));
redisService.deleteObject(lock);
}
实际测试:我用apifox(接口测试工具)使用多个线程调用接口,发现查询key的时候,多个线程会出现同时查不到key的情况。
这样会出现什么问题呢?通过key是否存在来判断是否上锁,不准确(没卵用),因为3个线程同时进去的时候,都还没有设置这个key,所以他们3个都会执行业务逻辑。那么这是为啥呢?有没有大佬给回答一下?按道理说,用户的点击速度不可能比redis设置key的速度快,但这样的结果显然不能接受。
解决问题:虽然没搞懂为啥,但是找到了解决方案,那就是通过setNx方法的返回值来判断是否上锁。奇怪的是:多个线程同时访问redis,拿到的值的时效性可能不准确,但他们却能准确的知道值是否存在。
工具类方法补充:
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}