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的购买信息保存到数据库中。

 

posted @ 2020-04-14 12:20  北漂的雷子  阅读(895)  评论(0编辑  收藏  举报