超卖现象一:

  一个库存,产生两个订单

  

 

解决方法:

  扣减库存不在程序中进行,而是通过数据库

  1.  向数据库传递库存增量,扣减一个库存,增量为-1
  2.  向数据库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.  扣减库存不在程序中进行,而是通过数据库
  2.  向数据库传递库存增量,扣减一个库存,增量为-1
  3.  在数据库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;

 

posted on 2022-07-01 11:53  draven*liu  阅读(83)  评论(0)    收藏  举报