• 多线程环境下事务与锁的问题


    简述

    最近在研究多线程的问题,使用Jmeter模拟多用户同时对同一商品进行下单。发现会出现超卖现象,然后尝试加锁,synchronized和Lock都试过,但是还是出现超卖现象。现在来复盘记录一下问题。

    数据库

    商品表

    1. CREATE TABLE `product` (
    2. `id` int(11) NOT NULL AUTO_INCREMENT,
    3. `name` varchar(255) DEFAULT NULL,
    4. `amount` int(255) DEFAULT NULL,
    5. PRIMARY KEY (`id`)
    6. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

    订单表

    1. CREATE TABLE `myorder` (
    2. `id` int(11) NOT NULL AUTO_INCREMENT,
    3. `order_name` varchar(255) DEFAULT NULL,
    4. `pid` int(11) DEFAULT NULL COMMENT '产品id',
    5. PRIMARY KEY (`id`)
    6. ) ENGINE=InnoDB AUTO_INCREMENT=5843 DEFAULT CHARSET=utf8mb4;

    Java代码

    1. @Transactional(rollbackFor = Exception.class)
    2. @Override
    3. public synchronized void aaa(int id){
    4. try {
    5. log.info(Thread.currentThread().getId()+"准备lock");
    6. String threadName = Thread.currentThread().getId()+"--"+System.currentTimeMillis();
    7. product productEntity = productDao.selectById(id);
    8. log.info(threadName+"查询到的商品库存是"+productEntity.getAmount());
    9. if (productEntity == null) {
    10. throw new RuntimeException("没有找到该商品");
    11. }
    12. int stock = productEntity.getAmount() - 1;
    13. if (stock >= 0) {
    14. productEntity.setAmount(stock);
    15. productDao.updateById(productEntity);
    16. orderDao.insert(myorder.builder().orderName(threadName).pid(id).build());
    17. } else {
    18. throw new RuntimeException("库存不足");
    19. }
    20. log.info(threadName + "结束任务");
    21. }catch (Exception e){
    22. e.printStackTrace();
    23. throw new RuntimeException("尝试抛出异常");
    24. }
    25. }

    简单逻辑说明:对相关信息进行日志打印,查询到商品不为0则进行下单操作,同时库存-1更新。但是结果如下:

    可以看到,有想当一部分的线程获取到同样的数据,这是意料之外的。这时候我在想是不是我的锁用得不对,然后我换了Lock锁,代码如下:

    1. @Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ)
    2. @Override
    3. public void aaa(int id) throws InterruptedException {
    4. lock.lock();
    5. try {
    6. log.info(Thread.currentThread().getName()+"准备lock");
    7. String threadName = Thread.currentThread().getName()+"--"+System.currentTimeMillis();
    8. product productEntity = productDao.selectById(id);
    9. log.info(threadName+"查询到的商品库存是"+productEntity.getAmount());
    10. if (productEntity == null) {
    11. throw new RuntimeException("没有找到该商品");
    12. }
    13. int stock = productEntity.getAmount() - 1;
    14. if (stock >= 0) {
    15. productEntity.setAmount(stock);
    16. orderDao.insert(myorder.builder().orderName(threadName).pid(id).build());
    17. int r = productDao.updateById(productEntity);
    18. } else {
    19. throw new RuntimeException("库存不足");
    20. }
    21. log.info(threadName + "结束任务");
    22. }catch (Exception e){
    23. e.printStackTrace();
    24. throw new RuntimeException("尝试抛出异常");
    25. }finally {
    26. lock.unlock();
    27. }
    28. }

    这段代码执行结果如下:

    一看!我了个去,不对劲啊,怎么那么多线程读同一个数据。按道理来说,应该加了锁的不会出现这种情况的啊,既然Lock和synchronized都会出现这种情况,仔细想想应该不是锁的问题。既然不是锁的问题那就是事务问题了,然后把

    1. @Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ)

    换成了

    1. @Transactional(rollbackFor = Exception.class,isolation = Isolation.SERIALIZABLE)

    换了串行化之后,这种读取同一个数据的问题就消失了。但是串行化的效率好低,只能够另某路径了。 通过一轮分析调试,我产生了一个疑问,既然是存在锁,会不会是因为锁释放了,但是事务还没来得及提交,然后锁被另外并发的线程拿到了,然后在一瞬间读取到了上一个事务还没提交的数据呢? 既然事务来不及提交,那就我来让它提交,然后再释放锁,将上述代码修改成手动提交/回滚事务

    1. @Autowired
    2. private PlatformTransactionManager platformTransactionManager;
    3. @Autowired
    4. private TransactionDefinition transactionDefinition;
    5. //@Transactional(rollbackFor = Exception.class)
    6. @Override
    7. public synchronized void aaa(int id){
    8. //开启事务
    9. TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
    10. try {
    11. log.info(Thread.currentThread().getId()+"准备lock");
    12. String threadName = Thread.currentThread().getId()+"--"+System.currentTimeMillis();
    13. product productEntity = productDao.selectById(id);
    14. log.info(threadName+"查询到的商品库存是"+productEntity.getAmount());
    15. if (productEntity == null) {
    16. throw new RuntimeException("没有找到该商品");
    17. }
    18. int stock = productEntity.getAmount() - 1;
    19. if (stock >= 0) {
    20. productEntity.setAmount(stock);
    21. productDao.updateById(productEntity);
    22. orderDao.insert(myorder.builder().orderName(threadName).pid(id).build());
    23. //手动提交事务
    24. platformTransactionManager.commit(transactionStatus);
    25. //log.info(threadName+"操作,商品减库存成功 剩余:" + stock);
    26. } else {
    27. throw new RuntimeException("库存不足");
    28. }
    29. log.info(threadName + "结束任务");
    30. }catch (Exception e){
    31. e.printStackTrace();
    32. //手动回滚事务
    33. platformTransactionManager.rollback(transactionStatus);
    34. //throw new RuntimeException("尝试抛出异常");
    35. }
    36. }

    执行效果如下:

    再看看数据库

    ok,完美。

    小结

    在多线程环境下,事务操作不能一直依赖事务注解@Transactional,必要时还是需要手动提交事务,以免出现锁释放了但是事务没提交的情况。具体情况结合自身业务进行调试解决。

  • 相关阅读:
    作为一名大学生,需要什么?前辈真实经历
    第3讲:MySQL数据库中常见的几种表字段数据类型
    职场人的拖延症晚癌克星来啦 当当狸时间管理器
    很多人都在考的PMP认证到底有什么用?考试内容难不难?
    实战二十六:基于字符串匹配的实体对齐任务 代码+数据 (可作为毕设)
    VB.NET之SqlCommand详解
    Redis 面试题汇总(不定期更新)
    基于element-ui封装可配置表单组件
    界面重建——Marching cubes算法
    【附源码】计算机毕业设计JAVA校园跑腿平台
  • 原文地址:https://blog.csdn.net/m0_57042151/article/details/126827727