点击上方“程序员蜗牛g”,选择“设为星标”跟蜗牛哥一起,每天进步一点点
程序员蜗牛g大厂程序员一枚 跟蜗牛一起 每天进步一点点32篇原创内容**公众号
目前仓库里目前有 100 本某热门小说的库存,结果有 1000 名读者在同一时间点访问你的网站,并尝试下单购买这本书,下面接口来处理购买请求:
@Transactionalpublic void buy(Long id, Integer quantity) { Product product = productRepository.findById(id).get() ; if (product.getQuantity() >= quantity) { product.setQuantity(product.getQuantity() - quantity); productRepository.save(product) ; } else { throw new RuntimeException("库存不足"); }}
2.实战案例
2.1 定义实体
@Entity@Table(name = "t_product")public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name ; private BigDecimal price ; private int quantity ; // getters, setters}
2.2 Repository接口使用@Lock
public interface ProductRepository extends JpaRepository<Product, Long> { @Transactional @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT p FROM Product p WHERE p.id = ?1") Product findByIdWithLock(Long id);}
注意:这里必须使用 @Transactional 注解,开启事务(当然,你也可以在Service中调用的方法上开启)。
当我们调用上面的findByIdWithLock方法,执行的sql如下:
自动在SQL上添加了 for update;当一个事务执行带有 FOR UPDATE 子句的查询时,数据库会锁定查询结果集中的行,阻止其他事务对这些行进行修改或锁定。
通过如下代码测试并发方法:
@Transactionalpublic Product queryProduct(Long id) { System.err.printf("[%d] %s - start...%n", System.currentTimeMillis(), Thread.currentThread().getName()) ; Product product = this.productRepository.findByIdWithLock(id); try { System.err.printf("[%d] %s - 锁定数据【%d】成功...%n", System.currentTimeMillis(), Thread.currentThread().getName(), id) ; TimeUnit.SECONDS.sleep(10) ; } System.err.printf("[%d] %s - end...%n", System.currentTimeMillis(), Thread.currentThread().getName()) ; return product ;}
当我有2个线程同时此接口时,输出如下:
当有线程锁定了给定ID的数据后,其它的线程必须等待锁释放以后才能锁定数据。
2.3 锁超时
悲观锁可能会导致死锁或长时间的等待,我们可以通过如下的配置设置悲观锁超时时间:
@Transactional@Lock(LockModeType.PESSIMISTIC_WRITE)@QueryHints({ @QueryHint( name = "jakarta.persistence.lock.timeout", value = "3000") // 3秒超时})@Query("SELECT p FROM Product p WHERE p.id = ?1")Product findByIdWithLock(Long id);
我们将基于MySQL进行测试上面的超时配置。测试代码为上面的queryProduct方法。
MySQL
多个线程同时访问,控制台输出:
并没有发生锁超时异常。
那么MySQL中如何处理悲观锁的超时呢?
mysql有一个配置 innodb_lock_wait_timeout ,在innodb引擎下锁等待超时配置,默认值如下:
默认50s超时事件,我们只能通过修改该超时时间来在程序中捕获异常。
通过如下修改超时时间:
新开窗口,再次查看是否生效:
以上修改是临时的,如果MySQL重启后就失效了,如下方式永久修改(在 MySQL 配置文件(通常是 my.cnf 或 my.ini)中添加或修改以下行:):
如上配置完后,我们再次运行mysql的测试:
抛出了悲观锁异常。
2.4 优雅处理悲观锁异常
在处理悲观锁异常时,优雅地处理异常可以帮助提高应用程序的健壮性和用户体验。在你的代码中,当发生 PessimisticLockingFailureException 时,可以采取一些策略来应对锁竞争,例如重试、记录日志、通知用户等。
接下来,我们将通过重试机制来改造代码
首先,引入依赖
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId></dependency>
其次,开启重试功能
@SpringBootApplication@EnableRetrypublic class App {}
最后,改造业务代码
@Transactionalpublic void buyProcess(Long id, Integer quantity) { Product product = productRepository.findByIdWithLock(id) .orElseThrow(() -> new ProductNotFoundException("商品不存在")); try { System.err.printf("[%d] %s - 锁定数据【%d】成功...%n", System.currentTimeMillis(), Thread.currentThread().getName(), id) ; TimeUnit.SECONDS.sleep(13) ; } if (product.getQuantity() < quantity) { throw new RuntimeException("库存不足"); } product.setQuantity(product.getQuantity() - quantity); productRepository.save(product);}// 重试全部失败后的降级处理@Recoverpublic void recover(PessimisticLockingFailureException e, Long id, Integer quantity) { // 记录日志、发送告警或抛出业务异常 throw new BusinessException("系统繁忙, 请稍后重试");}
接下来,我们通过2个线程进行测试,最终控制台输出如下:
经过2次重试后,成功。
当重试3次后,还是失败后:
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!