有一个用redis去重的情景。当时业务代码如下:
public void handle(String key){
// 判断集合中是否存在key元素
if(redis.sismember("myset",key)){
return;
}else{
// 如果不存在则添加key元素到myset集合中
redis.sadd("myset",key);
}
// 业务代码
伪代码的含义就是:如果集合中已经存在了元素key,那么直接返回;若不存在,则向集合myset中添加元素key,执行业务代码。
咋一看好像没啥问题,但是由于存在并发的情况,会导致了业务数代码重复执行。
这里假设有两个线程同时调用handle方法,入参key都为007。执行顺序如下:
| 线程一 | 线程二 |
|---|---|
| redis.sismember("myset",key)返回false | |
| 切换 | redis.sismember("myset",key)返回false |
| 执行redis.sadd("myset",007); | |
| 切换 | |
| 执行redis.sadd("myset",007); | |
| 执行业务代码 | |
| 执行业务代码 |
线程一调用的redis.sismember("myset",key)的时候,显然集合中没有007,返回false,跳转到else。这个时候时候线程一的时间片用完了,切换到线程二。
线程二也开始调用redis.sismember("myset",key),由于线程一还没有执行添加方法,所有这里集合中还是不存在007这个元素,还是返回false,然后线程二执行sadd("myset",key)方法添加007元素,然后线程二再次切换到线程一,线程一继续向下执行sadd("myset",key),添加007元素。
这里就会出现多次执行业务代码,导致数据出现问题。
虽然redis的单个命令都能保证原子性,但是上面伪代码的“如果不存在就添加(put-if-absent)”的操作存在竞态条件。虽然可以通过加锁来保证该方法的原子性,
但是由于这里使用的是redis,而redis正好提供了一个原子命令setnx key value,如果不存在key,则设置,如果存在,则什么也不做;所以这里只需要修改伪代码。
// 修改后的伪代码
public void handle(String key){
if(!redis.setnx(key,0)){
return;
}
// 业务代码
留下一个问题,如果这里的集合换成是线程安全的vector,那么该如何修改呢?