1. 引入并发问题1.1 项目搭建

这里以一个最基础的库存问题引入:在高并发下下单会造成库存数据异常情况。

数据表:就一个最基础的库存表和一个基础的数据。

mysql行锁和死锁检测(高并发使用JVM锁和MySQL锁解决数据不一致问题)(1)

mysql行锁和死锁检测(高并发使用JVM锁和MySQL锁解决数据不一致问题)(2)

2. 新建SpringBoot2.7.3项目并引入相关依赖:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

  1. mapper

public interface StockMapper extends BaseMapper<Stock> { }

  1. service

@Service public class StockServiceImpl implements StockService { @Resource private StockMapper stockMapper; @Override public Integer deStock() { Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>() .eq("product_code", "1001")); if (!Objects.isNull(stock) && stock.getCount() > 0){ stock.setCount(stock.getCount() -1); stockMapper.updateById(stock); } return stockMapper.selectById(stock.getId()).getCount(); } }

2.controller

@RestController @RequestMapping("/stock") public class StockController { @Resource private StockService stockService; @GetMapping public String deStock(){ return "库存剩余:" stockService.deStock(); } }

1.2 使用Jmeter进行测试

mysql行锁和死锁检测(高并发使用JVM锁和MySQL锁解决数据不一致问题)(3)

mysql行锁和死锁检测(高并发使用JVM锁和MySQL锁解决数据不一致问题)(4)

2. JVM锁解决

这里直接不测试了,肯定可以解决。

修改service减库存方法:

synchronized

@Override public synchronized Integer deStock() { Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>() .eq("product_code", "1001")); if (!Objects.isNull(stock) && stock.getCount() > 0){ stock.setCount(stock.getCount() -1); stockMapper.updateById(stock); } return stockMapper.selectById(stock.getId()).getCount(); }

ReentrantLock 显示锁

@Override public Integer deStock() { ReentrantLock reentrantLock = new ReentrantLock(); try { reentrantLock.lock(); Stock stock = stockMapper.selectOne(new QueryWrapper<Stock>() .eq("product_code", "1001")); if (!Objects.isNull(stock) && stock.getCount() > 0){ stock.setCount(stock.getCount() -1); stockMapper.updateById(stock); } return stockMapper.selectById(stock.getId()).getCount(); }finally { reentrantLock.unlock(); } }

JVM锁缺陷:总结来说:上诉缺陷除了能避免使用多例模式,其他两种在系统构建上是无法进行取代的,因此需要使用其他的方式来进行并发数据的处理。3. MySQL实现锁3.1 MySQL默认锁

使用MySQL中自带的锁去解决:MySQL在执行更、删、改操作时会自动对当前语句加锁,也就是说我们只要能够使用一条sql来实现当前功能就可以避免数据问题。

@Update("update db_stock set count = count - #{count} " "where product_code = #{productCode} and count >= #{count}") Integer deduct(@Param("productCode") String productCode, @Param("count") Integer count);

修改service实现类方法:

@Override public Integer deStock() { return stockMapper.deduct("1001", 1); }

3.2 MySQL悲观锁

@Select("select * from db_stock where product_code = #{productCode} for update") List<Stock> selectStockForUpdate(String productCode);

改造 service 中方法:

@Transactional @Override public Integer deStock() { List<Stock> stocks = stockMapper.selectStockForUpdate("1001"); if (Objects.isNull(stocks) || stocks.isEmpty()){ return -1; } // 假设存在多仓库情况,默认扣减第一个仓库 Stock stock = stocks.get(0); if (!Objects.isNull(stock) && stock.getCount() >= 1){ stock.setCount(stock.getCount() - 1); } return stockMapper.updateById(stock); }

3.3 MySQL乐观锁

乐观锁:默认对IO属性操作不加锁,在执行完毕对数据中的版本号或者其他属性进行判断,确定当前数据执行前后是否被其他的事务更改。也就是CAS思想。

CAS:Compare and Swap,比较并交换,其实就是有用一个属性,在更新后判断当前属性是否有变化,有变化就放弃更改,无变化就更改。

修改 service 方法

@Override public Integer deStock() { List<Stock> stocks = stockMapper.selectList( new QueryWrapper<Stock>() .eq("product_code", "1001")); if (Objects.isNull(stocks) || stocks.isEmpty()){ return -1; } // 假设存在多仓库情况,默认扣减第一个仓库 Stock stock = stocks.get(0); Integer version = 0; if (!Objects.isNull(stock) && stock.getCount() >= 1){ version = stock.getVersion(); stock.setCount(stock.getCount() - 1); stock.setVersion(version 1); } QueryWrapper<Stock> queryWrapper = new QueryWrapper<>(); queryWrapper .eq("id", stock.getId()) .eq("version", version); int update = stockMapper.update(stock, queryWrapper); // 更新失败递归重试 if (update == 0){ try { // 避免一直重试导致栈内存溢出 Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } return deStock(); } return update; }

version

4. JVM和MySQL锁总结,