最近在研究多线程的问题,使用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;
-
- @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,必要时还是需要手动提交事务,以免出现锁释放了但是事务没提交的情况。具体情况结合自身业务进行调试解决。