场景
我经常使用Redis,比如有一个常见的场景就是获取key的值,如果小于某个阈值,就加一并且将加一后的值重新set回redis,返回true,否则返回false。就这样简单额场景,其中也牵扯到线程安全的问题。
摊牌了,其实一些复杂的与Redis交互业务逻辑用LUA脚本可以保证原子性。
源码下载
Demooo/springboot-demo/src/main/java/com/example/redisthreadsafe at master · cbeann/Demooo · GitHub
线程不安全举例
下面的代码基本就是大众的逻辑,但是有些代码在并发情况下,就会出现错误。以下面代码为例子,如果请求超过阈值LIMIT=10,请求就返回0。
现在考虑这样的一种的一种情况,两个线程同时第一次访问该接口,即大家到步骤2的时候num都是0,那么同时继续往下,那是不是这两个线程执行完毕后,你却发现redis里值为1 ,这就出现了线程不安全的问题。
代码语言:javascript复制 @GetMapping("/notThreadSafe")
public Object notThreadSafe() {
//步骤1:拼接key
int foodId = 1;
String key = "stock:" foodId;
//阈值3
final int LIMIT = 10;
//步骤2:获取redis里存的值
String s = stringRedisTemplate.opsForValue().get(key);
int num = 0;
if (!StringUtils.isEmpty(s)) {
num = Integer.parseInt(s);
}
//步骤3:主判断逻辑
if (num < LIMIT) {
num ;
stringRedisTemplate.opsForValue().set(key, String.valueOf(num));
return 1;
}
return 0;
}
加锁synchronized
单实例线程安全没有问题,多实例还是数据不一致。
代码语言:javascript复制@GetMapping("/singleInstanceThreadSafe")
public Object notThreadSafe() {
//步骤1:拼接key
int foodId = 1;
String key = "stock:" foodId;
//阈值3
final int LIMIT = 10;
synchronized (this){
//步骤2:获取redis里存的值
String s = stringRedisTemplate.opsForValue().get(key);
int num = 0;
if (!StringUtils.isEmpty(s)) {
num = Integer.parseInt(s);
}
//步骤3:主判断逻辑
if (num < LIMIT) {
num ;
stringRedisTemplate.opsForValue().set(key, String.valueOf(num));
return 1;
}
return 0;
}
}
加分布式锁:伪代码
参考:基于redis的分布式锁_CBeann的博客-CSDN博客
加锁的问题就是性能低,具有排他性
程安全实例:基于Lua脚本
lua脚本,所有的命令为原子性
代码语言:javascript复制--根据key判断是否存在
local key = redis.call("EXISTS", KEYS[1])
--存在key
if tonumber(key) == 1 then
--获取key的值
local number = redis.call("GET", KEYS[1])
--key的值小于阈值
if tonumber(number) < tonumber(ARGV[1]) then
redis.call("incrby", KEYS[1], ARGV[2])
return 1
else
return 0
end
else
--不存在
redis.call("SET", KEYS[1], ARGV[2])
return 1
end
Java代码
代码语言:javascript复制@GetMapping("/threadSafe")
public Object threadSafe() {
//步骤1:拼接key
int foodId = 1;
String key = "stock:" foodId;
//阈值3
final int LIMIT = 10;
DefaultRedisScript<Object> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(Object.class);
defaultRedisScript.setScriptText("--根据key判断是否存在n"
"local key = redis.call("EXISTS", KEYS[1])n"
"--存在keyn"
"if tonumber(key) == 1 thenn"
" --获取key的值n"
" local number = redis.call("GET", KEYS[1])n"
" --key的值小于阈值n"
" if tonumber(number) < tonumber(ARGV[1]) thenn"
" redis.call("incrby", KEYS[1], ARGV[2])n"
" return 1n"
" elsen"
" return 0n"
" endn"
"n"
"elsen"
" --不存在n"
" redis.call("SET", KEYS[1], ARGV[2])n"
" return 1n"
"end");
List<String> keys = new ArrayList<>();
keys.add(key);
Object[] args = new Object[2];
args[0] = LIMIT;//阈值
args[1] = 2;//每次加几
Object execute = redisTemplate.execute(defaultRedisScript, keys, args);
System.out.println(execute);
return execute;
}