如何应对高并发:悲观锁,乐观锁,Redis

您所在的位置:网站首页 net如何应对高并发 如何应对高并发:悲观锁,乐观锁,Redis

如何应对高并发:悲观锁,乐观锁,Redis

2024-07-12 20:43| 来源: 网络整理| 查看: 265

根据上一篇Demo测试情况反映,当有多个线程同时抢购时,会发生超发现象,所谓超发现象,就是原本设置库存为30000件,但是,当抢购完成后发现库存余量变成了负数,即发货量大于库存量的情况:

12115116-6eaa1b534204cb63.png

超发现象

造成这种现象的原因:当多个线程请求数据库查询库存余量时,显示有余量,但是当进行扣减库存时,库存已经用完了,但那个线程并不知道,依旧去扣减库存,造成库存为负数的情况,于是乎就出现了超发现象。

测试方法:根据书上是html中使用js,for循环异步请求,发现并不会造成超发现象,后改为在浏览器中同时开启多个窗口访问/test进行抢购,模拟多个用户抢购的场景,内存爆炸...

为了解决这种问题,下面将介绍三种解决方法:

1、悲观锁

发生超发现象的根本原因是共享数据被多个线程所修改,无法保证其执行顺序,如果一个数据库事务读取到一个产品后,就将数据直接锁定,不允许其他线程进行读写操作,直至当前数据库事务完成才释放这条数据的锁,就不会发生超发现象,但是执行效率性能将大大下降。

修改ProductMapper中的SQL语句:

@Mapper public interface ProductMapper { @Select("SELECT id, product_name as productName, stock, price, version, note FROM t_product where id=#{id} for update") ProductPo getProduct(Long id); @Update("UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{id}") int decreaseProduct(@Param("id") Long id, @Param("quantity") int quantity); }

在select语句末尾添加了for update,这样,在数据库事务执行的过程中,就会锁定查询出来的数据,其他事务将不能再对其进行读写,单个请求直至数据库事务完成,才会释放这个锁,下图可见其stock为0,没有发生超发现象,但执行效率下降了,通过购买记录可以得知,相比之前没加锁慢了1/5。

12115116-35055e6f055d3943.png

库存

2、乐观锁

为了解决悲观锁带来的性能下降的问题,我们来讨论一下乐观锁的原理:

乐观锁是一种不使用数据库锁和不阻塞线程并发的方案,下图是以本Demo为例的乐观锁流程:

12115116-d4fcb19cfd5eebb3.png

流程图

这种方案就是多线程的概念CAS(Compare and Swap),然而这样的方案会引发一种ABA问题:

T1时刻:线程1读取商品库存为A

T2时刻:线程2读取商品库存为A

T3时刻:线程2计算购买商品总价格

T4时刻:当前库存为A,与线程2保存的旧值一致,因此线程2可减库存(当前库存A--->B),此时线程1在当前库存为B的情况下计算剩余商品价格(单价*B)。

T5时刻:线程2取消购买,线程2回退(当前库存B--->A),线程1计算的剩余商品价格错误。

T6时刻:线程1比较旧值与当前数据库库存,发现都为A,返回之前计算好的(单价*B)结果,造成了错误。

从上面的分析中看到一个现象A--->B--->A的过程,就是所谓的ABA问题,解决此问题的方法为加入版本号的限制,只要在操作过程中修改共享值,无论业务正常,回退,还是异常,版本号只能递增,不能回退递减。每次通过比较数据的版本号来查看此数据是否被修改过。

@Mapper public interface ProductMapper { @Select("SELECT id, product_name as productName, stock, price, version, note FROM t_product where id=#{id}") ProductPo getProduct(Long id); //********************change****************************** @Update("UPDATE t_product SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{id} and version = #{version}") int decreaseProduct(@Param("id") Long id, @Param("quantity") int quantity, @Param("version") int version); } @Override // 启动Spring数据库事务机制 @Transactional public boolean purchase(Long userId, Long productId, int quantity) { // 获取产品 ProductPo product = productMapper.getProduct(productId); // 比较库存和购买数量 if (product.getStock() < quantity) { // 库存不足 return false; } //**************************change******************************* // 扣减库存,加入了version productMapper.decreaseProduct(productId, quantity, product.getVersion()); //*************************************************************** // 初始化购买记录 PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity); // 插入购买记录 purchaseRecordMapper.insertPurchaseRecord(pr); return true; }

12115116-f10d6a8fe4f0d96a.png

产品表

发现stock并没有降为0,原因是加入了版本号的判断,所以大量的请求得到了失败的结果,而且失败率有点高。要解决这个方法,就设定为如果失败,就重试,直至成功,但是这样又会造成大量SQL执行,影响性能,所以一般可以使用限制时间或者重入次数的方法来克服。

时间戳限制重入的乐观锁:

将一个请求限制在100ms的生存期,如果在100ms内发生版本号冲突而导致不能更新的,则会重新尝试请求,否则请求失败。

修改service下PurchaseserviceImpl的purchase方法

@Override // 启动Spring数据库事务机制 @Transactional public boolean purchase(Long userId, Long productId, int quantity) { long start = System.currentTimeMillis(); while (true){ long end = System.currentTimeMillis(); if (end - start >100){ return false; } // 获取产品 ProductPo product = productMapper.getProduct(productId); // 比较库存和购买数量 if (product.getStock() < quantity) { // 库存不足 return false; } // 扣减库存 int result = productMapper.decreaseProduct(productId, quantity, product.getVersion()); // 如果数据更新失败,说明数据在多线程中被其他线程修改 // 导致失败,着通过循环重入尝试购买商品 if (result == 0){ continue; } // 初始化购买记录 PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity); // 插入购买记录 purchaseRecordMapper.insertPurchaseRecord(pr); return true; } }

这种方法在测试中效果并不是很好,执行速度很慢,冲突现象并没有减少,反而增多,可能是我测试方法并不好,只开了三个网页来模拟并发,不太懂JS,Demo用的JS是发送异步请求的,但用单窗口测试了好多次都没出现超发现象,只能人肉模拟并发。

限定次数重入的乐观锁:

@Override // 启动Spring数据库事务机制 @Transactional public boolean purchase(Long userId, Long productId, int quantity) { long start = System.currentTimeMillis(); for (int i=0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3