共计 2704 个字符,预计需要花费 7 分钟才能阅读完成。
场景:
在日常开发中经常遇到先根据条件判断某条数据是否存在,如果不存在的话就插入,如果存在的话就更新或提示异常。一般代码的模式都写成下面这个样子,是一种很常见的写法,但是在并发情况下很容易会重复插入两条数据,大概的情况就是第一个请求进来,没有查询到该用户通过了 if
判断,但是 if
中有比较耗时的逻辑,在第一个请求还没执行 insert
的时候第二个请求也进来了,因为这个时候第一个请求还没执行 insert
操作,所以第二个请求也没有查询到该用户也通过了 if
判断,这个样子就造成了两条重复的数据。
// 查询名字叫 user1 的用户是否存在
UserVo userVo= userMapper.selectUserByName("user1");
// 如果不存在就插入数据
if (userVo==null) {Thread.sleep(10000);
UserVo userVo = new UserVo();
userVo.setUserName("user1");
userMapper.insert(userVo);
}
}
解决方法:
1. 使用 synchronized
同步代码块
直接将查询校验逻辑和插入逻辑都进行同步,也就是说第一个请求的逻辑没结束,第二个请求就会一直等待着,只有当第一个请求执行完同步代码块中的逻辑释放锁后第二个请求才能获取到锁执行这段逻辑。
private Object obj = new Object();
synchronized (object){
// 查询名字叫 user1 的用户是否存在
UserVo userVo= userMapper.selectUserByName("user1");
// 如果不存在就插入数据
if (userVo==null) {Thread.sleep(10000);
UserVo userVo = new UserVo();
userVo.setUserName("user1");
userMapper.insert(userVo);
}
}
2. 使用 Lock
锁
其实和 synchronized
代码块是相同的作用,但是要注意必须在 finally
中释放锁,避免出现异常死锁了。
private Lock lock = new ReentrantLock();
try {lock.lock();
// 查询名字叫 user1 的用户是否存在
UserVo userVo = userMapper.selectUserByName("user1");
// 如果不存在就插入数据
if (userVo == null) {Thread.sleep(10000);
UserVo userVo = new UserVo();
userVo.setUserName("user1");
userMapper.insert(userVo);
}
} finally {lock.unlock();
}
3. 给数据库索引
既然是要根据用户名字判断是否有重复数据,所以可以直接在数据库上给 userName
字段添加 UNIQUE
索引,这样在第二次重复插入的时候就会提示异常。如果不想重复插入的时候有报错提示可以使用 INSERT IGNORE INTO
语句。而代码则不必做任何逻辑操作。
// 查询名字叫 user1 的用户是否存在
UserVo userVo= userMapper.selectUserByName("user1");
// 如果不存在就插入数据
if (userVo==null) {Thread.sleep(10000);
UserVo userVo = new UserVo();
userVo.setUserName("user1");
userMapper.insert(userVo);
}
}
4. 使用 redis
中setnx
来作为锁
redis
中 setnx
命令是只有当你存入的 key
不存在时才会成功存入,并返回 1,而如果 key
已经存在的时候则存入失败并返回 0,我们可以拿这个特性来当做锁。首先这个方法进来第一步就是执行 setnx
操作,把查询的用户名存入 redis
,然后查询该用户是否存在,第一个请求进到if
判断中但是没执行插入逻辑,第二个请求虽然也没有查询到该用户,但是它的 setnx
会失败,因为第一个请求存的 key
还没删除,所以这样就避免了并发重新插入的问题,而且最大的优点就是它不像 synchronized
和Lock
无论所有请求进来都只能一个一个通过,使用这种方法是只有当操作同一个用户有并发请求的时候才会阻塞,而如果是请求两个不同的用户时是不会阻塞的,都可以顺利通过,因为存入的 key
是不同的。
// 自动注入 spring 的 redis 操作类
@Autowired
private RedisTemplate redisTemplate;
public String addUser (String userName) {
// 执行 setnx 命令,存入当前拿来判断的用户名
BoundValueOperations operations = redisTemplate.boundValueOps(userName);
// 执行 setnx 命令的结果,这里封装的方法是直接返回 true 和 false
boolean addFlag = operations.setIfAbsent(1);
// 返回结果
String result = null;
UserVo userVo= userMapper.selectUserByName(userName);
try {if (userVo == null && addFlag == true) {Thread.sleep(10000);
UserVo userVo = new UserVo();
userVo.setUserName("user1");
userMapper.insert(userVo);
} else{result = "更新失败";}
} finally {
// 无论更新成功和失败都去删除 setnx 添加的 key
operations.getOperations().delete(userName);
}
return result;
}
总结:
上述四种方法,给数据库加索引、Lock
和 redis
都有使用过,synchronized
和 Lock
也差不多,个人感觉给数据库加索引来控制这种并发太死板了,万一系统中有其他地方的逻辑是需要重复添加这个字段的数据,这个时候就没办法使用索引了,synchronized
和 Lock
效率太低了,如果是并发量太大的这种方式肯定是不可缺的,而 redis 的这种方法则效率高很多,比较适合并发量高的操作。
结尾:
因为本人接触的系统的并发量也不是很大,所以对这方面的技术也是自己在钻研摸索,可能会有很多地方有遗漏和错误,如果大家有更好的方法欢迎一起留言讨论,也欢迎指出错误。