场景

  我经常使用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 ,这就出现了线程不安全的问题。

 @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

单实例线程安全没有问题,多实例还是数据不一致。

@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的博客-​

加锁的问题就是性能低,具有排他性

程安全实例:基于Lua脚本

lua脚本,所有的命令为原子性

--根据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代码 

@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"
+ "--存在key\n"
+ "if tonumber(key) == 1 then\n"
+ " --获取key的值\n"
+ " local number = redis.call(\"GET\", KEYS[1])\n"
+ " --key的值小于阈值\n"
+ " if tonumber(number) < tonumber(ARGV[1]) then\n"
+ " redis.call(\"incrby\", KEYS[1], ARGV[2])\n"
+ " return 1\n"
+ " else\n"
+ " return 0\n"
+ " end\n"
+ "\n"
+ "else\n"
+ " --不存在\n"
+ " redis.call(\"SET\", KEYS[1], ARGV[2])\n"
+ " return 1\n"
+ "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;


}