zoukankan      html  css  js  c++  java
  • 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的购买信息保存到数据库中。

  • 相关阅读:
    Scala中隐式转换(implicit conversion)的优先顺序
    protege4.0使用中的理论
    国外程序员整理的 C++ 资源大全
    什么是本体论?
    深入分析C++引用
    在基于对话框的MFC程序中,使程序在任务栏中不显示图标
    PhoneGap搭建运行环境(3.2版本)
    [JS代码]如何判断ipad或者iphone是否为横屏或者竖屏
    windwos iis 7.5 使用html 报405错误
    NodeJs 开源
  • 原文地址:https://www.cnblogs.com/leiziv5/p/12694541.html
Copyright © 2011-2022 走看看