學習筆記,按照《深入淺出 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的購買信息保存到數據庫中。