Java并发事务问题 2022-02-09 15:31:00 编程 Java 1 条评论 593 次阅读 4567字 修改时间:2022-02-24 10:12:51 ## 场景 代码片段,查询是否有用户“小明”,没有则插入。 模拟10个线程并发执行。 ```java @GetMapping("/test") public void test() { for (int i = 0; i < 10; i++) { new Thread(() -> { testService.save(); }).start(); } } ``` ```java @Transactional public void save(){ User user = userMapper.selectOne(Wrappers.lambdaQuery().eq(User::getName, "小明")); if (user == null) { User newUser = new User(); newUser.setName("小明"); newUser.setAge(18); userMapper.insert(newUser); log.info("保存成功"); }else{ log.info("已经存在"); } } ```  结果10个线程全部插入成功。。。 **解决方案** 1.数据库字段“name”加上唯一索引 2.代码层面控制并发 第一点在业务逻辑比较复杂的情况下,做起来并不简单,这里就不多描述。主要讲解第二点。 ------------ ### 解决1:使用 synchronized (失败) 直接给 save 方法加上 synchronized 关键字 ```java @Transactional public synchronized void save(){ User user = userMapper.selectOne(Wrappers.lambdaQuery().eq(User::getName, "小明")); if (user == null) { User newUser = new User(); newUser.setName("小明"); newUser.setAge(18); userMapper.insert(newUser); log.info("保存成功"); }else{ log.info("已经存在"); } } ```  可以发现,使用了 synchronized 后,依然有重复插入的现象 **原因** 这是因为 save() 方法上使用了 @Transaction 注解,开启事务时,Spring AOP会动态生成一个代理类,代理类调用当前类中的 save() 方法,在调用之前开启事务,结束后关闭事务。但是 synchronized 是作用于当前类的,也就是说,在代理类调用完save()方法后,事务并没有结束,但是此时 synchronized 作用域下的方法执行完后就释放掉了锁,下一个线程进入方法后不能读取到上个线程还未提交的事务(事务的隔离级别),所以导致锁失效 ------------ ### 解决2:使用 synchronized (成功) 既然 @Transaction 会导致 synchronized 失效,那就想办法避免 synchronized 加在事务方法上。 将操作数据库的逻辑代码单独抽出来,synchronized 加在非事务 save() 方法上,在 save() 中调用事务方法newSave()。这样的话,就能保证先提交事务,再释放锁。 ```java public class TestService { @Autowired private Test2Service test2Service static final String LOCK = ""; public void save(){ synchronized (LOCK){ test2Service.newSave(); } } } ``` ```java public class Test2Service { @Resource private UserMapper userMapper; @Transactional public void newSave(){ User user = userMapper.selectOne(Wrappers.lambdaQuery().eq(User::getName, "小明")); if (user == null) { User newUser = new User(); newUser.setName("小明"); newUser.setAge(18); userMapper.insert(newUser); log.info("保存成功"); }else{ log.info("已经存在"); } } } ```  这次开足马力,使用 Jmeter 开启300个线程,加上Controller 中10个线程, 300 * 10 = 3000 个并发请求同时插入,完美控制住了并发。 **提示** 这种方式虽然可以有效的控制住并发,但是弊端也很明显,就是事务单独提交了,如果上层代码发生异常,那newSave()中的事务也不能回滚了 ------------ ### 解决3:使用Redis 使用Redis实现锁,主要是利用其原子性,将某个资源放到Redis当中,当其他线程访问时,如果资源已经存在,就不允许之后的操作了。 其实只要是具有原子性,就都可以当做锁来使用,比如mysql的唯一索引等等 spring boot使用 Redis 的操作主要是通过 RedisTemplate 来实现,一般步骤如下: 1.将锁资源放入 Redis (注意是当key不存在时才能放成功,所以使用 setIfAbsent 方法): ```java redisTemplate.opsForValue().setIfAbsent("key", "value"); redisTemplate.expire("key", 30000, TimeUnit.MILLISECONDS); ``` 2.释放锁 ```java redisTemplate.delete("key"); ``` 这里有个问题,如果在调用 setIfAbsent 方法之后服务挂掉了,即没有给锁定的资源设置过期时间,默认是永不过期,那么这个锁就会一直存在。所以需要保证设置锁及其过期时间两个操作的原子性,spring data的 RedisTemplate 当中并没有这样的方法。可以使用jedis的这种原子操作方法,这里就不展开了。 ```java @Service public class TestService { @Resource private UserMapper userMapper; @Autowired private RedisTemplate redisTemplate; private boolean lock() { boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent("lock", "1"); redisTemplate.expire("lock", 5000, TimeUnit.MILLISECONDS); return setIfAbsent; } private void unLock() { redisTemplate.delete("lock"); } @Transactional public void save() { try { // 获取锁 if (lock()) { log.info("成功获取锁"); User user = userMapper.selectOne(Wrappers.lambdaQuery().eq(User::getName, "小明")); if (user == null) { User newUser = new User(); newUser.setName("小明"); newUser.setAge(18); userMapper.insert(newUser); } }else{ log.info("获取锁失败"); } } finally { //此处释放锁会和 synchronized 一样,事务在释放锁后提交 // unLock(); } } } ```  标签: Java
1、唯一索引方案不可取,明知并发且代码逻辑是先查后增。想象一下无控制代码逻辑,后台对并发请求一直爆mysql唯一索引错误。
2、若业务场景是并发,每次user对象不同(即非案例中单单全是小明),方案可以为先查询,查无则增,反之作其他处理。
以2方案来看,先可以分解查询和新增两个步骤,对于查询来说,若该表数据量不多查询场景较少,缓存无意义,若该表数据很多,即便查询每条数据很多,缓存也无意义,想象一张用户表有10W条数据,小明小兰等,总不能存10W条用户数据记录缓存吧?
故:每次请求后台走一次新增方法,对于查询无事务操作,进行结果辨别后若需新增再开启事务。