likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 8

简述

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

数据库

商品表

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

订单表

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

Java代码

@Transactional(rollbackFor = Exception.class)
@Override
public synchronized void aaa(int id){
    try {
    log.info(Thread.currentThread().getId()+"准备lock");
    String threadName = Thread.currentThread().getId()+"--"+System.currentTimeMillis();
    product productEntity = productDao.selectById(id);
    log.info(threadName+"查询到的商品库存是"+productEntity.getAmount());
    if (productEntity == null) {
        throw new RuntimeException("没有找到该商品");
    }
    int stock = productEntity.getAmount() - 1;
    if (stock >= 0) {
        productEntity.setAmount(stock);
        productDao.updateById(productEntity);
        orderDao.insert(myorder.builder().orderName(threadName).pid(id).build());
    } else {
        throw new RuntimeException("库存不足");
    }
    log.info(threadName + "结束任务");
}catch (Exception e){
    e.printStackTrace();
    throw new RuntimeException("尝试抛出异常");
}
}

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

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

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

@Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ)
@Override
public  void aaa(int id) throws InterruptedException {
        lock.lock();
        try {
            log.info(Thread.currentThread().getName()+"准备lock");
            String threadName = Thread.currentThread().getName()+"--"+System.currentTimeMillis();
               product productEntity = productDao.selectById(id);
               log.info(threadName+"查询到的商品库存是"+productEntity.getAmount());
               if (productEntity == null) {
                   throw new RuntimeException("没有找到该商品");
               }
               int stock = productEntity.getAmount() - 1;
               if (stock >= 0) {
                   productEntity.setAmount(stock);
                   orderDao.insert(myorder.builder().orderName(threadName).pid(id).build());
                   int r = productDao.updateById(productEntity);
               } else {
                   throw new RuntimeException("库存不足");
               }
            log.info(threadName + "结束任务");
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("尝试抛出异常");
        }finally {
           lock.unlock();
        }
}

这段代码执行结果如下:

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

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

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

换成了

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

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

@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
//@Transactional(rollbackFor = Exception.class)
@Override
public synchronized void aaa(int id){
    //开启事务
    TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
    try {
    log.info(Thread.currentThread().getId()+"准备lock");
    String threadName = Thread.currentThread().getId()+"--"+System.currentTimeMillis();
    product productEntity = productDao.selectById(id);
    log.info(threadName+"查询到的商品库存是"+productEntity.getAmount());
    if (productEntity == null) {
        throw new RuntimeException("没有找到该商品");
    }
    int stock = productEntity.getAmount() - 1;
    if (stock >= 0) {
        productEntity.setAmount(stock);
        productDao.updateById(productEntity);
        orderDao.insert(myorder.builder().orderName(threadName).pid(id).build());
        //手动提交事务
         platformTransactionManager.commit(transactionStatus);
        //log.info(threadName+"操作,商品减库存成功 剩余:" + stock);
    } else {
        throw new RuntimeException("库存不足");
    }
    log.info(threadName + "结束任务");
}catch (Exception e){
    e.printStackTrace();
    //手动回滚事务
    platformTransactionManager.rollback(transactionStatus);
    //throw new RuntimeException("尝试抛出异常");
}
}

执行效果如下:

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

再看看数据库

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

ok,完美。

小结

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