超卖现象一:
一个库存,产生两个订单

解决方法:
扣减库存不在程序中进行,而是通过数据库
- 向数据库传递库存增量,扣减一个库存,增量为-1
- 向数据库update语句计算库存,通过update行锁解决并发
创建3个表作为测试
CREATE TABLE `order` ( `id` int(11) NOT NULL, `order_status` varchar(255) DEFAULT NULL, `receive_time` datetime DEFAULT NULL, `receive_mobile` varchar(255) DEFAULT NULL, `order_time` datetime DEFAULT NULL, `create_user` varchar(255) DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `order_item` ( `order_id` int(11) NOT NULL, `product_id` int(11) DEFAULT NULL, `purchase_num` int(11) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `create_user` varchar(255) DEFAULT NULL, `update-time` datetime DEFAULT NULL, `update_user` datetime DEFAULT NULL, PRIMARY KEY (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `product` ( `id` int(11) NOT NULL, `product_name` varchar(255) DEFAULT NULL, `price` decimal(10,2) DEFAULT NULL, `count` int(11) DEFAULT NULL, `product_desc` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `create_user` varchar(255) DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `product`(`id`, `product_name`, `price`, `count`, `product_desc`, `create_time`, `create_user`, `update_time`) VALUES (100100, '测试商品', 5.00, 1, '测试商品', '2022-07-01 13:16:03', 'draven', '2022-07-01 13:16:10');
使用idea插件逆向工程生成相关代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Draven</artifactId>
<groupId>com.draven</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Demo001</artifactId>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<configuration>
<!--configurationFile对应着generator配置文件的路径-->
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
generatorConfig.xml 路径跟上面pom.xml 配置的保持一致
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressDate" value="true"/>
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!--数据库链接URL,用户名、密码 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/test" userId="root" password="root">
</jdbcConnection>
<javaTypeResolver>
<!-- 是否使用bigDecimal, false可自动转化以下类型(Long, Integer, Short, etc.) -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 生成实体类的包名和位置 ,targetPackage指的是包名,targetProject值得是路径位置-->
<javaModelGenerator targetPackage="com.draven.model" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成映射文件的包名和位置-->
<sqlMapGenerator targetPackage="com.draven.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- 生成DAO的包名和位置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.draven.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 要生成的表 tableName是数据库中的表名或视图名 domainObjectName是实体类名-->
<table tableName="order_item" domainObjectName="OrderItem" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
</context>
</generatorConfiguration>

生成对应的代码文件

贴上相关主要代码:
package com.draven.service;
import com.draven.dao.OrderItemMapper;
import com.draven.dao.OrderMapper;
import com.draven.dao.ProductMapper;
import com.draven.model.Order;
import com.draven.model.OrderItem;
import com.draven.model.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private ProductMapper productMapper;
//构买商品ID
private int pursecaseProductId = 100100;
//构买商品数量
private int purchaseProductNum = 1;
@Transactional(rollbackFor = Exception.class)
public Integer createOrder() throws Exception {
Product product = productMapper.selectByPrimaryKey(pursecaseProductId);
if (product == null) {
throw new Exception("构买商品:" + pursecaseProductId + "不存在");
}
//商品当前库存
Integer count = product.getCount();
//校验库存
if (purchaseProductNum > count) {
throw new Exception("商品" + pursecaseProductId + "仅剩" + count + "件,无法购买");
}
//计算剩余库存
Integer leftCount = count - purchaseProductNum;
//更新库存
product.setCount(leftCount);
product.setCreateTime(new Date());
product.setCreateUser("xxx");
productMapper.updateByPrimaryKeySelective(product);
/* Order order = new Order();
order.setId(1);
order.setReceiveMobile("111111");
order.setOrderStatus("11111");
orderMapper.insert(order);*/
OrderItem orderItem = new OrderItem();
// orderItem.setOrderId(order.getId());
orderItem.setProductId(product.getId());
orderItem.setPurchaseNum(purchaseProductNum);
orderItem.setCreateUser("xxx");
orderItem.setCreateTime(new Date());
orderItemMapper.insertSelective(orderItem);
return 1;
}
}
测试代码:
package com.draven;
import ch.qos.logback.core.helpers.CyclicBuffer;
import com.draven.service.OrderService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DistributeApplicationTest {
@Autowired
private OrderService orderService;
@Test
public void concurrentOrder() throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(5);
CyclicBarrier cyclicBarrier= new CyclicBarrier(5);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(() -> {
try {
cyclicBarrier.await();
Integer orderId = orderService.createOrder();
System.out.println("订单id" + orderId);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception exception) {
exception.printStackTrace();
}
});
}
cdl.await();
executorService.shutdown();
}
}
执行结果,我们数据库商品表的库存只有一个,但是会产生5个订单
解决方法:
- 扣减库存不在程序中进行,而是通过数据库
- 向数据库传递库存增量,扣减一个库存,增量为-1
- 在数据库update语句计算库存,通过update行锁解决并发
我们将扣减库存的操作,移交给数据库做增量的减库存(利用数据库的行锁特性
<update id="updateProductCount">
update product set count=count -#{purchaseProductNum,jdbcType=INTEGER},
create_user = #{createUser,jdbcType=VARCHAR},
update_time = #{updateTime,jdbcType=TIMESTAMP}
WHERE id = #{id,jdbcType=INTEGER}
</update>
但是上面的执行结果会出现,库存会出现负数。
超卖现象二
系统中库存变为-1
产生原因:
并发检验库存,造成库存充足的假象

解决方案:加锁
校验库存,扣减库存同意加锁
使之成为原子性的操作
并发时,只有获得锁的线程才能校验,扣减库存
扣减库存结束后,释放锁
确保库存不会扣成负数
这里我们使用手动提交的事务,因为当我们使用@Transactional(rollbackFor = Exception.class)注解使用的时候,我们并没有锁住事务,我们线程执行完了,但是数据库的事务不一定提交完成了,也可能会产生负数问题
@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
// @Transactional(rollbackFor = Exception.class)
public synchronized Integer createOrder() throws Exception {
final TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
Product product = productMapper.selectByPrimaryKey(pursecaseProductId);
if (product == null) {
throw new Exception("构买商品:" + pursecaseProductId + "不存在");
}
//商品当前库存
Integer count = product.getCount();
//校验库存
if (purchaseProductNum > count) {
platformTransactionManager.rollback(transaction);
throw new Exception("商品" + pursecaseProductId + "仅剩" + count + "件,无法购买");
}
//计算剩余库存
Integer leftCount = count - purchaseProductNum;
productMapper.updateProductCount(product.getId(), purchaseProductNum, "xxx", new Date());
OrderItem orderItem = new OrderItem();
// orderItem.setOrderId(order.getId());
orderItem.setProductId(product.getId());
orderItem.setPurchaseNum(purchaseProductNum);
orderItem.setCreateUser("xxx");
orderItem.setCreateTime(new Date());
orderItemMapper.insertSelective(orderItem);
platformTransactionManager.commit(transaction);
return 1;
浙公网安备 33010602011771号