spring boot 實現搶購商品


學習筆記,按照《深入淺出 Spring Boot 2.x》。
數據庫設計:
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for product -- ---------------------------- DROP TABLE IF EXISTS `product`; CREATE TABLE `product` ( `id` int(12) NOT NULL AUTO_INCREMENT COMMENT '編號', `name` varchar(255) NOT NULL COMMENT '產品名稱', `stock` int(10) NOT NULL COMMENT '庫存', `price` decimal(16,2) NOT NULL COMMENT '單價', `version` varchar(10) NOT NULL COMMENT '版本', `note` varchar(255) DEFAULT NULL COMMENT '備注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for proecudrecode -- ---------------------------- DROP TABLE IF EXISTS `proecudrecode`; CREATE TABLE `proecudrecode` ( `id` int(12) NOT NULL AUTO_INCREMENT COMMENT '編號', `userid` int(12) NOT NULL, `productid` int(12) NOT NULL COMMENT '產品編號', `price` decimal(10,2) NOT NULL, `quantity` int(255) NOT NULL COMMENT '數量', `sum` decimal(10,2) NOT NULL COMMENT '總價', `purchar` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '購買日期', `note` varchar(255) DEFAULT NULL COMMENT '備注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=601 DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS = 1;

   數據庫設計完畢后,我們去創建工程,這里用到mybatis,jpa,connect mysql等,pom.xml如下

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.product</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  我們來寫dto層

@Mapper
public interface ProductDao {
    public ProductPo getProduct(Long id);

    public int decreaseProduct(@Param("id") Long id,
                               @Param("quantity") int quantity);
}
@Mapper
public interface Puchaesre {
    public  int insertPurcha(PurchaseRecordPo purchaseRecordPo);
}

寫下po層

@Data
public class ProductPo implements Serializable {
    private  static  final  long serialVersionUID=328831147730635602L;
    private  Long id;
    private  String name;
    private  int stock;
    private  double price;
    private  int version;
    private  String note;
}
@Data
public class PurchaseRecordPo implements Serializable {
    private  static  final  long serialVersionUID=-360816189433370174L;
    private  long id;
    private  long userid;
    private  long productid;
    private  double price;
    private  int quantity;
    private  double sum;
    private Timestamp purchar;
    private  String note;

}

這樣我們去寫mybatis的文件,如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.product.demo.dao.ProductDao">
    <select id="getProduct" parameterType="long" resultType="ProductPo">
       SELECT id,`name`,stock,price,version,note FROM product WHERE id=#{id}
    </select>
    <update id="decreaseProduct">
        update  product set stock=stock-#{quantity} where  id=#{id}
    </update>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.product.demo.dao.Puchaesre">
    <insert id="insertPurcha" parameterType="PurchaseRecordPo">
insert  into  proecudrecode(userid,productid,price,quantity,`sum`,purchar,note) values
(#{userid},
#{productid},#{price},#{quantity},#{sum},now() ,#{note})
    </insert>

</mapper>

這里寫完了,之后呢,我們就要去開發我們的業務模塊了。

public interface PuseeSerice {
    public  boolean purchase(Long userId,Long productid,int quantity);
}

我們去實現下業務邏輯

@Service
public class PuseeserimIMpl implements PuseeSerice {
    @Autowired
    private ProductDao productDao;
    @Autowired
    private Puchaesre puchaesre;
    @Override
@Transactional
public boolean purchase(Long userId, Long productid, int quantity) {
        ProductPo productPo=productDao.getProduct(productid);
        if (productPo.getStock()<quantity){
            return false;
        }
        productDao.decreaseProduct(productid,quantity);
        PurchaseRecordPo purchaseRecordPo=initpush(userId,productPo,quantity);
        puchaesre.insertPurcha(purchaseRecordPo);
        return true;
    }
    private  PurchaseRecordPo initpush(Long userid,ProductPo productPo,int quantity){
        PurchaseRecordPo purchaseRecordPo=new PurchaseRecordPo();
        purchaseRecordPo.setNote("購買時間,"+System.currentTimeMillis());
        purchaseRecordPo.setPrice(productPo.getPrice());
        purchaseRecordPo.setProductid(productPo.getId());
        purchaseRecordPo.setQuantity(quantity);
        double sum=productPo.getPrice()*quantity;
        purchaseRecordPo.setSum(sum);
        purchaseRecordPo.setUserid(userid);
        return  purchaseRecordPo;
    }
}

實現后,我們去實現我們的api層

@RestController
public class PurchaseCpntroller {
    @Autowired
    PuseeSerice puseeSerice;
    @PostMapping("/purchese")
    public  Result oyrchase(Long userid,Long projectid,Integer quantity){
        boolean sucse=puseeSerice.purchase(userid,projectid,quantity);
        String message=sucse? "搶購成功":"搶購失敗";
        Result result=new Result(sucse,message);
        return result;
    }
}

class Result{
    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    private  boolean success=false;
    private  String message=null;
    public  Result(boolean success,String message){
        this.message=message;
        this.success=success;
    }
}

接下來我們去配置下,我們的啟動類

package com.example.product.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.product.demo.dao")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

我們配置了下application.yaml

 

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url:  jdbc:mysql://localhost:3306/product?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username:  root
    password:
    tomcat:
      max-active: 50
      max-idle: 10
      max-wait: 10000
      initial-size: 5
      default-transaction-isolation: 2
mybatis:
  type-aliases-package: com.example.product.demo.pojo
  mapper-locations: classpath:map/*.xml

接下來就是啟動下

 

 

調試下,沒有問題,我們去壓測下,因為正常情況下我們需要壓測我們的接口,我們用下jMeter,

 

 

 我們去並發請求,

 

 

 

肯定有成功,有失敗,我們去看下,我們的數據庫,。

 

 

 

我們發現,我們的商品發超了。可能是在扣庫存的其他的線程也在操作,沒有做區分,就導致了超發,這樣我們可以用樂觀鎖 悲觀鎖,或者reids來實現。

我們實現下悲觀鎖,

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.product.demo.dao.ProductDao">
    <select id="getProduct" parameterType="long" resultType="ProductPo">
       SELECT id,`name`,stock,price,version,note FROM product WHERE id=#{id} for  update
    </select>
    <update id="decreaseProduct">
        update  product set stock=stock-#{quantity} where  id=#{id}
    </update>
</mapper>

  這樣就實現了悲觀鎖,我們來測試下,

 

 

 我們看下數據庫,

 

 沒有出現超發現象,但是出現了性能有所下降的問題,可以去查看購買記錄,但是這樣能保證我們的發售的商品不超賣,犧牲一些性能的,

 我們看下樂觀鎖,用樂觀鎖來實現下,我們使用版本號字段來控制,版本號增加,扣庫存,

   <update id="decreaseProduct">
        update  product set stock=stock-#{quantity},version=version+1 where  id=#{id} and version=#{version}
    </update>

對應mapper修改

@Mapper
public interface ProductDao {
    public ProductPo getProduct(Long id);

    public int decreaseProduct(@Param("id") Long id,
                               @Param("quantity") int quantity,
                               @Param("version") int version);
}

修改邏輯代碼

@Override
    @Transactional
    public boolean purchase(Long userId, Long productid, int quantity) {
        ProductPo productPo=productDao.getProduct(productid);
        if (productPo.getStock()<quantity){
            return false;
        }
        int version=productPo.getVersion();
        int reslut=productDao.decreaseProduct(productid,quantity,version);
        if (reslut==0){
            return  false;
        }
        PurchaseRecordPo purchaseRecordPo=initpush(userId,productPo,quantity);
        puchaesre.insertPurcha(purchaseRecordPo);
        return true;
    }

完成后,我們去修改下調試下,然后進行並發壓測,

 

 我們發現了,錯誤率上升了,看下記錄,發現部分記錄沒有增加進去。但是庫存扣減了,我們這個時候可以利用增加重入次數,來對錯誤的進行重試。

@Override
    @Transactional
    public boolean purchase(Long userId, Long productid, int quantity) {
        for(int i=0;i<3;i++){
            ProductPo productPo=productDao.getProduct(productid);
            if (productPo.getStock()<quantity){
                //庫存不足
                return false;
            }
            //獲取版本號
            int version=productPo.getVersion();
            int reslut=productDao.decreaseProduct(productid,quantity,version);
            //扣庫存失敗
            if (reslut==0){
                //重試
                continue;
            }
            PurchaseRecordPo purchaseRecordPo=initpush(userId,productPo,quantity);
            puchaesre.insertPurcha(purchaseRecordPo);
            return true;
        }
        return  false;
    }

這樣增加重試機制后,錯誤次數減少。  今個是可以發現,其實上這樣操作是保證了扣減庫存的增強,但是一般在企業中 通常考慮用NoSQl作為解決方案,比較常用的是redis,大概的思路是

利用redis響應高並發的用戶請求

定時任務將redis的購買信息保存到數據庫中。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM