简述 最近在研究多线程的问题,使用Jmeter模拟多用户同时对同一商品进行下单。发现会出现超卖现象,然后尝试加锁,synchronized和Lock都试过,但是还是出现超卖现象。现在来复盘记录
简述
最近在研究多线程的问题,使用Jmeter模拟多用户同时对同一商品进行下单。发现会出现超卖现象,然后尝试加锁,synchronized和Lock都试过,但是还是出现超卖现象。现在来复盘记录一下问题。
数据库
基于Hadoop与Spark的大数据开发实战商品表
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)@Overridepublic 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)@Overridepublic 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)换了串行化之后,这种读取同一个数据的问题就消失了。但是串行化的效率好低,只能够另某路径了。 通过一轮分析调试,我产生了一个疑问,既然是存在锁,会不会是因为锁释放了,但是事务还没来得及提交,然后锁被另外并发的线程拿到了,然后在一瞬间读取到了上一个事务还没提交的数据呢? 既然事务来不及提交,那就我来让它提交,然后再释放锁,将上述代码修改成手动提交/回滚事务
@Autowiredprivate PlatformTransactionManager platformTransactionManager;@Autowiredprivate TransactionDefinition transactionDefinition;//@Transactional(rollbackFor = Exception.class)@Overridepublic 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,必要时还是需要手动提交事务,以免出现锁释放了但是事务没提交的情况。具体情况结合自身业务进行调试解决。