mysql 乐观锁 超卖_通过乐观锁解决库存超卖的问题

本文介绍了如何使用MySQL乐观锁解决高并发场景下的库存超卖问题。通过创建商品和订单表,设置并发版本控制字段,实现在扣减库存时进行版本检查,确保数据一致性。同时提供了一个Service层的实现示例,展示了并发测试的方法,最终验证乐观锁能有效避免超卖现象。
摘要由CSDN通过智能技术生成

前言

在通过多线程来解决高并发的问题上,线程安全往往是最先需要考虑的问题,其次才是性能。库存超卖问题是有很多种技术解决方案的,比如悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作等。本篇通过MySQL乐观锁来演示基本实现。

开发前准备

1. 环境参数

开发工具:IDEA

基础工具:Maven+JDK8

所用技术:SpringBoot+Mybatis

数据库:MySQL5.7

SpringBoot版本:2.2.5.RELEASE

2. 创建数据库

基本的scheme已建好,演示就拿最简单的数据结构最好不过了。

DROP TABLE IF EXISTS `goods`;

CREATE TABLE `goods` (

`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品id',

`name` varchar(30) DEFAULT NULL COMMENT '商品名称',

`stock` int(11) DEFAULT '0' COMMENT '商品库存',

`version` int(11) DEFAULT '0' COMMENT '并发版本控制',

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '商品表';

INSERT INTO `goods` VALUES (1, 'iphone', 10, 0);

INSERT INTO `goods` VALUES (2, 'huawei', 10, 0);

DROP TABLE IF EXISTS `order`;

CREATE TABLE `order` (

`id` int(11) AUTO_INCREMENT,

`uid` int(11) COMMENT '用户id',

`gid` int(11) COMMENT '商品id',

PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '订单表';

没有环境的小伙伴可以通过Docker实战之MySQL主从复制,快速的进行MySQL环境的搭建。创建数据库test,然后导入相关的sql初始化Table。

3. 配置 pom 文件中的相关依赖

下边是pom.xml依赖配置。

org.springframework.boot

spring-boot-starter-web

org.mybatis.spring.boot

mybatis-spring-boot-starter

2.1.1

org.springframework.boot

spring-boot-devtools

runtime

true

mysql

mysql-connector-java

runtime

org.projectlombok

lombok

true

org.springframework.boot

spring-boot-starter-test

test

4. 配置 application.yml

由于演示中MyBatis基于接口映射,配置简单。application.yml中只需要配置mysql相关即可

spring:

datasource:

type: com.zaxxer.hikari.HikariDataSource

driverClassName: com.mysql.cj.jdbc.Driver

url: jdbc:mysql://localhost:3307/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC

username: root

password: root

5. 创建相关Bean

package com.idcmind.ants.entity;

public class Goods {

private int id;

private String name;

private int stock;

private int version;

...

此处省略getter、setter以及 toString方法

}

public class Order {

private int id;

private int uid;

private int gid;

...

此处省略getter、setter以及 toString方法

}

乐观锁解决库存超卖方案

1. Dao层开发

GoodsDao.java

@Mapper

public interface GoodsDao {

/**

* 查询商品库存

* @param id 商品id

* @return

*/

@Select("SELECT * FROM goods WHERE id = #{id}")

Goods getStock(@Param("id") int id);

/**

* 乐观锁方案扣减库存

* @param id 商品id

* @param version 版本号

* @return

*/

@Update("UPDATE goods SET stock = stock - 1, version = version + 1 WHERE id = #{id} AND stock > 0 AND version = #{version}")

int decreaseStockForVersion(@Param("id") int id, @Param("version") int version);

}

OrderDao.java

这里需要特别注意,由于order是sql中的关键字,所以表名需要加上反引号。

@Mapper

public interface OrderDao {

/**

* 插入订单

* 注意: order表是关键字,需要`order`

* @param order

*/

@Insert("INSERT INTO `order` (uid, gid) VALUES (#{uid}, #{gid})")

@Options(useGeneratedKeys = true, keyProperty = "id")

int insertOrder(Order order);

}

2. Service层开发

GoodsService.java

@Service

public class GoodsService {

@Autowired

private GoodsDao goodsDao;

@Autowired

private OrderDao orderDao;

/**

* 扣减库存

* @param gid 商品id

* @param uid 用户id

* @return SUCCESS 1 FAILURE 0

*/

@Transactional

public int sellGoods(int gid, int uid) {

// 获取库存

Goods goods = goodsDao.getStock(gid);

if (goods.getStock() > 0) {

// 乐观锁更新库存

int update = goodsDao.decreaseStockForVersion(gid, goods.getVersion());

// 更新失败,说明其他线程已经修改过数据,本次扣减库存失败,可以重试一定次数或者返回

if (update == 0) {

return 0;

}

// 库存扣减成功,生成订单

Order order = new Order();

order.setUid(uid);

order.setGid(gid);

int result = orderDao.insertOrder(order);

return result;

}

// 失败返回

return 0;

}

}

并发测试

这里我们写个单元测试进行并发测试。

@SpringBootTest

class GoodsServiceTest {

@Autowired

GoodsService goodsService;

@Test

void seckill() throws InterruptedException {

// 库存初始化为10,这里通过CountDownLatch和线程池模拟100个并发

int threadTotal = 100;

ExecutorService executorService = Executors.newCachedThreadPool();

final CountDownLatch countDownLatch = new CountDownLatch(threadTotal);

for (int i = 0; i < threadTotal ; i++) {

int uid = i;

executorService.execute(() -> {

try {

goodsService.sellGoods(1, uid);

} catch (Exception e) {

e.printStackTrace();

}

countDownLatch.countDown();

});

}

countDownLatch.await();

executorService.shutdown();

}

}

查看数据库验证是否超卖

0b62ac0dd2c98b12e17030910ef8cc1f.png

上图的结果与我们的预期一致。此外还可以通过Postman或者Jmeter进行并发测试。由于不是此处的重点,不再做演示,感兴趣的小伙伴可以留言,我会整理下相关的教程。

后续

这篇文章通过数据库乐观锁已经解决了库存超卖的问题,不过效率上并不是最优方案,后续会完善其他方案的演示。文中如有错漏之处,还望大家不吝赐教。

公众号【当我遇上你】

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值