当前位置:   article > 正文

@Transactional 中使用线程锁导致了锁失效_在事务中使用锁无效

在事务中使用锁无效


概述

很多小伙伴使用Spring事务时,为了省事都喜欢使用@Transactional。但是@Transactional配合锁,会导致一些预期之外的问题!
在此举例说明。

数据准备

针对下文用例,定义:
数据库表:

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(16) COLLATE utf8mb4_bin NOT NULL COMMENT 'name',
  `age` int NOT NULL COMMENT 'age',
  `level` int NOT NULL COMMENT 'level',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'

INSERT INTO `user` (`id`, `name`, `age`, `level`) VALUES ('1','Tom','19','100');
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

实体类:

public class User{
	private Long id;
  	private String name;
    private Integer age;
    private Integer level;
    // 略get/set方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述

@Transactional是如何导致锁失效的

不加锁

Service层代码

@Service
public class UserService {

     @Autowired
     private UserMapper mapper;

     public void getAndReduceLevel() {
         User user = mapper.selectById(1L);

         User updateDTO = new User();
         updateDTO.setId(1L);
         updateDTO.setLevel(user.getLevel()-1);
         mapper.updateById(updateDTO);
     }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

测试用例:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {CustomerApplication.class})
public class ConcurrencyTest {

    @Autowired
    private UserService userService;

    @Test
    public void concurrencyTest() {
        for (int i = 0; i < 10; i++) {
            new Thread(() ->  {
                for (int j = 0; j < 10; j++) {
                    userService.getAndReduceLevel();
                }
            }).start();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

输出结果:
在这里插入图片描述
执行结果:我们发现,level只扣减了16,说明存在并发问题!

使用锁

@Service
public class UserService {
     private Lock lock = new ReentrantLock();
     @Autowired
     private UserMapper mapper;
     
     public void getAndReduceLevel() {
         try{
             // 加锁
             lock.lock();
             
             User user = mapper.selectById(1L);

             User updateDTO = new User();
             updateDTO.setId(1L);
             updateDTO.setLevel(user.getLevel()-1);
             mapper.updateById(updateDTO);
         } finally {
             lock.unlock();
         }
     }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

输出结果:
在这里插入图片描述
通过执行结果,我们发现,使用锁是可以控制并发问题。

使用锁+@Transactional

@Service
public class UserService {
     private Lock lock = new ReentrantLock();
     @Autowired
     private UserMapper mapper;
     @Transactional
     public void getAndReduceLevel() {
         try{
             // 加锁
             lock.lock();
             
             User user = mapper.selectById(1L);

             User updateDTO = new User();
             updateDTO.setId(1L);
             updateDTO.setLevel(user.getLevel()-1);
             mapper.updateById(updateDTO);
         } finally {
             lock.unlock();
         }
     }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

输出结果:
在这里插入图片描述通过执行结果,我们发现,level只扣减了54!用了@Transactional之后,锁怎么就失效了呢!

问题分析

我们都知道,@Transactional是通过使用AOP,在目标方法执行前后进行事务的开启和提交。所以,Lock锁住的代码,其实并没有包含住一整个事务!
通过下面的图理解一下:
在这里插入图片描述当线程A将level设置为99时,此时锁已经释放了,但是事务还没提交!!线程B此时可以获取到锁并进行查询,查询出来的level还是线程A修改之前的100,所以出现了并发问题。

解决方案

  1. @Transactional单独一个方法
    业务层代码
    public void lockMethod() {
        try {
            // 加锁
            lock.lock();
            UserService userService = (UserService) AopContext.currentProxy();
            userService.getAndReduceLevel();
        } finally {
            lock.unlock();
        }
    }
    
    @Transactional
    public void getAndReduceLevel() {
        User user = mapper.selectById(1L);
    
        User updateDTO = new User();
        updateDTO.setId(1L);
        updateDTO.setLevel(user.getLevel()-1);
        mapper.updateById(updateDTO);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

输出结果:
在这里插入图片描述
没有并发问题出现!

  1. 使用编程式事务
    业务层代码

    private Lock lock = new ReentrantLock();
    @Autowired
    private PlatformTransactionManager transactionManager;
    public void getAndReduceLevel() {
        try {
            // 加锁
            lock.lock();
            TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
            User user = mapper.selectById(1L);
    
            User updateDTO = new User();
            updateDTO.setId(1L);
            updateDTO.setLevel(user.getLevel()-1);
            mapper.updateById(updateDTO);
            transactionManager.commit(status);
        } finally {
            lock.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出结果:
    在这里插入图片描述

    编程式事务也能锁住了。

总结

在@Transactional注解中使用线程锁(例如synchronized关键字或ReentrantLock)确实可能导致一些与事务和锁相关的行为不如预期。这种情况主要出现在多个线程或事务并发访问共享资源时。
锁与事务的粒度不匹配,线程锁通常用于同步访问某个对象的临界区,确保同一时间只有一个线程能够执行这段代码。而事务则是用于确保数据库操作的原子性、一致性、隔离性和持久性。如果锁的粒度太粗,可能会阻止其他线程进行无关的事务操作,导致性能下降。如果锁的粒度太细,又可能无法正确同步关键的业务逻辑。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/1020914
推荐阅读
相关标签
  

闽ICP备14008679号