SpringBoot简单设计一个秒杀系统流程

SpringBoot简单设计一个秒杀系统流程

文章目录

  • 一、秒杀系统
    • 1.1 秒杀的场景
    • 1.2 保护的方法
  • 二、防止超卖
    • 1.数据库的设计
    • 2.pojo实体层
    • 3.Mapper.xml
    • 4.Mapper层
    • 5.Service层
    • 6.Controller层
    • 7.正常测试
    • 8.使用Jmeter进行压力测试
    • 9.解决超卖现象
      • 1.第一种:悲观锁 加同步代码块 效率低
      • 2. 第二种:乐观锁 Version版本 效率高
  • 三 .接口限流
  • 四. 限时抢购的实现
  • 五. 抢购接口隐藏(MD5加密)
    • 1.创建用户表
    • 2. User实体层
    • 3. UserMapper
    • 4.UserMapper.xml
    • 5.OrderServiceImpl 生成MD5签名
    • 2. Controller层添加生成MD5方法
    • 3. 测试MD5
    • 4.Controller创建一个携带md5下单接口
    • 5. OrderService层重载kill()方法
    • 6.OrderServiceImpl 实现层
    • 7. 测试
  • 六. 单用户限制频率
    • 1.UserService接口
    • 2.UserServiceImpl 实现类
    • 3.Controler创建接口
    • 4.Jmeter压力测试
  • 七. GitHub源码


一、秒杀系统

1.1 秒杀的场景

  1. 淘宝限量抢购商品
  2. 火车票(12306系统)

1.2 保护的方法

  1. 乐观锁防止超卖
  2. 令牌桶限流
  3. Redis 缓存
  4. MD5加密,隐藏秒杀接口

二、防止超卖

1.数据库的设计

创建两张表 商品信息stock 和 stock_order 订单表

DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) NOT NULL COMMENT '名称',`count` int(11) NOT NULL COMMENT '库存',`sale` int(11) NOT NULL COMMENT '已售',`version` int(11) NOT NULL COMMENT '乐观锁,版本号',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (`id` int(11) NOT NULL AUTO_INCREMENT,`sid` int(11) NOT NULL COMMENT '库存ID',`name` varchar(255) NOT NULL COMMENT '商品名称',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=596 DEFAULT CHARSET=utf8;

2.pojo实体层

Order 订单

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Order {private Integer id;private Integer sid;private String name;private Date createTime;
}

Stock 商品信息

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Stock {private Integer id;private String name;private Integer count;private Integer sale;private Integer version;
}

3.Mapper.xml

StockMapper.xml 商品信息



<mapper namespace="com.xizi.miaosha.mapper.StockMapper"><select id="checkStock" parameterType="INT" resultType="com.xizi.miaosha.pojo.Stock">select id,name,count,sale,version from stockwhere id=#{id}select><update id="updateStock"   parameterType="com.xizi.miaosha.pojo.Stock" >update stock setsale=#{sale}whereid=#{id}update>
mapper>

Ordermapper.xml 创建订单



<mapper namespace="com.xizi.miaosha.mapper.OrderMapper"><insert id="createOrder" parameterType="com.xizi.miaosha.pojo.Order" useGeneratedKeys="true" keyProperty="id">insert into stock_order values (#{id},#{sid},#{name},#{createTime});insert>mapper>

4.Mapper层

订单的mapper

package com.xizi.miaosha.mapper;import com.xizi.miaosha.pojo.Order;
import org.springframework.stereotype.Repository;@Repository
public interface OrderMapper {/*创建订单*/int createOrder(Order order);
}

商品信息的mapper

package com.xizi.miaosha.mapper;import com.xizi.miaosha.pojo.Stock;
import org.springframework.stereotype.Repository;@Repository
public interface StockMapper {//根据商品id查询库存信息的方法Stock checkStock(Integer id);/*根据商品扣除库存*/int updateStock(Stock stock);
}

5.Service层

OrderService接口

package com.xizi.miaosha.service;/*** 订单业务*/
public interface OrderService {/*处理秒杀的下单方法*/Integer kill(Integer id);
}

OrderServiceImpl 实现类

package com.xizi.miaosha.service.impl;@Service
@Transactional
@Slf4j
public class OrderServiceImpl implements OrderService {@Autowiredprivate StockMapper stockMapper;@Autowiredprivate OrderMapper orderMapper;@Overridepublic  Integer  kill(Integer id) {//根据商品id效验ku库存Stock stock = checkStock(id);//扣除库存updateSale(stock);//创建订单Integer orderId = createOrder(stock);return orderId;}//效验库存private Stock checkStock(Integer id){Stock stock = stockMapper.checkStock(id);if(stock.getSale().equals(stock.getCount())){throw new RuntimeException("库存不足!!!");}return stock;}//扣除库存//在业务层进行商品售卖+1操作private void updateSale(Stock stock){stock.setSale(stock.getSale()+1);int updateRows = stockMapper.updateStock(stock);if(updateRows==0){throw new RuntimeException("抢购失败,请重试!!!");}}//创建订单private Integer createOrder(Stock stock){Order order = new Order();order.setSid(stock.getId()).setName(stock.getName()).setCreateTime(new Date());orderMapper.createOrder(order);return order.getId();}
}

6.Controller层

@RestController
@RequestMapping("/stock")
@Slf4j
public class StockController {@Autowiredprivate OrderService orderService;//开发秒杀方法  @RequestMapping(value = "/kill0",method = RequestMethod.GET)public  String kill0(Integer id){try {System.out.println("秒杀商品的id: "+id);//根据秒杀的商品id 去调用秒杀业务Integer orderId = orderService.kill(id);return "秒杀成功,订单id为:"+orderId;} catch (Exception e) {e.printStackTrace();return e.getMessage();}}}

7.正常测试

在这里插入图片描述

正常测试没有问题

在这里插入图片描述

8.使用Jmeter进行压力测试

配置当前接口 配置了2000个线程数

  • jmeter压力测试 jmeter -n -t [文件地址]

在这里插入图片描述

出现超卖现象 卖出了143 大于实际的20

在这里插入图片描述

在这里插入图片描述

9.解决超卖现象

1.第一种:悲观锁 加同步代码块 效率低

    //开发秒杀方法  使用悲观锁防止超卖@RequestMapping(value = "/kill0",method = RequestMethod.GET)public  String kill0(Integer id){try {//悲观锁  同步代码块 同步执行 效率降低//保证当前线程得执行比事务大synchronized(this){System.out.println("秒杀商品的id: "+id);//根据秒杀的商品id 去调用秒杀业务Integer orderId = orderService.kill(id);return "秒杀成功,订单id为:"+orderId;}} catch (Exception e) {e.printStackTrace();return e.getMessage();}}

Jmeter测试 2000线程数 解决商品超卖

在这里插入图片描述


注意事项

如果使用synchronized方法进行同步处理 该业务上使用了注解 @Transactional 会出现超卖问题异常问题 @Transactional 事务注解带有同步得功能 当前事务同步能力大于 synchronized方法

2. 第二种:乐观锁 Version版本 效率高

实际上是把主要防止超卖问题交给数据库解决,利用数据库中定义的version字段以及数据库中的事务实现在并发情况下商品的超卖问题


在OrderServiceImpl 修改扣除库存方法

    //扣除库存private void updateSale(Stock stock){//在sql层面完成销量的+1 和版本号的+1  并且根据商品id和版本号同时查询更新的商品int updateRows = stockMapper.updateStock(stock);if(updateRows==0){throw new RuntimeException("抢购失败,请重试!!!");}}

修改StockMapper.xml 更新库存方法

<update id="updateStock"   parameterType="com.xizi.miaosha.pojo.Stock" >update stock setsale=sale+1 ,version=version+1whereid=#{id} andversion=#{version}
</update>

添加新的接口进行测试

    //开发秒杀方法  使用乐观锁防止超卖@RequestMapping(value = "/kill",method = RequestMethod.GET)public  String kill(Integer id){try {//悲观锁  同步代码块 同步执行 效率降低//保证当前线程得执行比事务大
//            synchronized(this){System.out.println("秒杀商品的id: "+id);//根据秒杀的商品id 去调用秒杀业务Integer orderId = orderService.kill(id);return "秒杀成功,订单id为:"+orderId;
//            }} catch (Exception e) {e.printStackTrace();return e.getMessage();}}

测试结果 解决商品超卖问题

在这里插入图片描述

三 .接口限流

限流:是对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机

  1. 1 接口限流

在面临高并发的抢购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力。大量的请求抢购成功时需要调用下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。

  1. 2 如何解决接口限流

常用的限流算法有令牌桶和和漏桶(漏斗算法),而Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
  • 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
  1. 3 令牌桶和漏斗算法
  • 漏斗算法:漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
  • 令牌桶算法:最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。

3.4 令牌桶简单使用

  1. 导入依赖
     <dependency><groupId>com.google.guavagroupId><artifactId>guavaartifactId><version>28.2-jreversion>dependency>

在Controller层添加方法

//创建令牌桶的实例//每次令牌桶放行10个请求private RateLimiter rateLimiter= RateLimiter.create(10);//开发秒杀方法  使用乐观锁防止超卖+令牌桶算法限流@RequestMapping(value = "/killtoken",method = RequestMethod.GET)public  String killtoken(Integer id){//1.没有获取到token请求一直知道获取到token 令牌//log.info("等待的时间: "+  rateLimiter.acquire());//加入令牌桶的限流措施//2.设置一个等待时间,如果在等待的时间内获取到了token 令牌,则处理业务,如果在等待时间内没有获取到响应token则抛弃//设置超过两秒没有拿到令牌  抛弃请求if(!rateLimiter.tryAcquire(2,TimeUnit.SECONDS )){log.info("抛弃的请求:抢购失败,当前秒杀活动过于火爆,请重试");return "抢购失败,当前秒杀活动过于火爆,请重试";}System.out.println("秒杀商品的id: "+id);try {//根据秒杀的商品id 去调用秒杀业务Integer orderId = orderService.kill(id);System.out.println("秒杀成功,订单id为:"+orderId);return "秒杀成功,订单id为:"+orderId;} catch (Exception e) {e.printStackTrace();return e.getMessage();}}

测试 令牌桶算法限流

在这里插入图片描述

四. 限时抢购的实现

使用Redis来记录秒杀商品的时间,对秒杀过期的请求进行拒绝处理!


  1. 启动 Redis 服务

在这里插入图片描述

  1. 将秒杀商品放入Redis并设置超时
    这里我们使用String类型 以kill + 商品id作为key 以商品id作为value,设置180秒超时(可随意设置时间)
    在这里插入图片描述

127.0.0.1:6379> set kill1 1 EX 180
OK

  1. 导入Redis依赖
<dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-data-redisartifactId>
dependency>
  1. 修改配置连接redis
	spring.redis.port=6379spring.redis.host=localhostspring.redis.database=0
  1. 使用redis控制抢购超时的请求 OrderServiceImpl层中修改
@Service
@Transactional
@Slf4j
public class OrderServiceImpl implements OrderService {@Autowiredprivate StockMapper stockMapper;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate UserMapper userMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic  Integer  kill(Integer id) {
//        校验redis中秒杀商品是否超时if(!stringRedisTemplate.hasKey("kill"+id)){throw new RuntimeException("当前商品的抢购活动已经结束了----");}//根据商品id效验ku库存Stock stock = checkStock(id);//扣除库存updateSale(stock);//创建订单Integer orderId = createOrder(stock);return orderId;}

测试 在设置的180s后秒杀接口关闭

在这里插入图片描述

五. 抢购接口隐藏(MD5加密)

防止抢购的链接被不断地请求,只要稍微写点爬虫代码,模拟一个抢购请求,就可以不通过点击下单按钮,直接在代码中请求我们的接口,完成下单,可以写一些脚本抢购各种秒杀商品。

将抢购接口进行隐藏,抢购接口隐藏(接口加盐)的具体做法:

  • 每次点击秒杀按钮,先从服务器获取一个秒杀验证值(接口内判断是否到秒杀时间)。
  • Redis以缓存用户ID和商品ID为Key,秒杀地址为Value缓存验证值
  • 用户请求秒杀商品的时候,要带上秒杀验证值进行校验。

在这里插入图片描述

1.创建用户表

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`password` varchar(255) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

2. User实体层

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class User {private Integer id;private  String name;private String password;
}

3. UserMapper

package com.xizi.miaosha.mapper;import com.xizi.miaosha.pojo.User;
import org.springframework.stereotype.Repository;@Repository
public interface UserMapper {//通过id 查询用户User findById(Integer id);
}

4.UserMapper.xml



<mapper namespace="com.xizi.miaosha.mapper.UserMapper"><select id="findById" parameterType="Integer" resultType="User">select id,name,password from user where id=#{id}select>
mapper>

5.OrderServiceImpl 生成MD5签名

@Service
@Transactional
@Slf4j
public class OrderServiceImpl implements OrderService {@Autowiredprivate StockMapper stockMapper;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate UserMapper userMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;// //生成md5签名得方法@Overridepublic String getMd5(Integer id, Integer userId) {//验证userId 用户是否存在User user = userMapper.findById(userId);if(user==null){throw new RuntimeException("用户信息不存在");}log.info("用户的信息:[{}]",user.toString());//验证商品id  存放商品信息Stock stock = stockMapper.checkStock(id);if(stock==null){throw new RuntimeException("商品信息不存在");}log.info("商品的信息:[{}]",stock.toString());//生成md5签名 放入redis 服务String hashKey="KEY_"+userId+id;//随机盐String key = DigestUtils.md5DigestAsHex((userId + id + "!XIZIzz").getBytes());stringRedisTemplate.opsForValue().set(hashKey, key,240, TimeUnit.SECONDS);log.info("Redis写入:[{}] [{}]",hashKey,key);return key;}

2. Controller层添加生成MD5方法

@RestController
@RequestMapping("/stock")
@Slf4j
public class StockController {@Autowiredprivate OrderService orderService;//生成md5值得方法@RequestMapping("md5")public String getMd5(Integer id,Integer userId){String md5;try {md5=orderService.getMd5(id,userId);} catch (Exception e) {e.printStackTrace();return "获取md5失败: "+e.getMessage();}return "获取md5信息为: "+md5;}

3. 测试MD5

在这里插入图片描述

4.Controller创建一个携带md5下单接口

   //开发秒杀方法  使用乐观锁防止超卖+令牌桶算法限流+md5加密(id+userId)
//    抢购接口隐藏  不能直接访问 必须先进行MD5加密存入redis 在请求接口的时候比较MD5是否相等@RequestMapping(value = "/killtokenmd5",method = RequestMethod.GET)public  String killtokenmd5(Integer id,Integer userId,String md5){System.out.println("秒杀商品的id: "+id);//加入令牌桶的限流措施if(!rateLimiter.tryAcquire(2,TimeUnit.SECONDS )){log.info("抛弃的请求:抢购失败,当前秒杀活动过于火爆,请重试");return "抢购失败,当前秒杀活动过于火爆,请重试";}try {//根据秒杀的商品id 去调用秒杀业务Integer orderId = orderService.kill(id,userId,md5);System.out.println("秒杀成功,订单id为:"+orderId);return "秒杀成功,订单id为:"+orderId;} catch (Exception e) {e.printStackTrace();return e.getMessage();}}

5. OrderService层重载kill()方法

/*** 订单业务*/
public interface OrderService {/*处理秒杀的下单方法*/Integer kill(Integer id);//生成md5签名得方法String getMd5(Integer id, Integer userId);//用来处理秒杀的下单方法 并返回订单id 加入 md5接口Integer kill(Integer id, Integer userId, String md5);
}

6.OrderServiceImpl 实现层

    //用来处理秒杀的下单方法 并返回订单id 加入 md5接口@Overridepublic Integer kill(Integer id, Integer userId, String md5) {
//        校验redis中秒杀商品是否超时if(!stringRedisTemplate.hasKey("kill"+id)){throw new RuntimeException("当前商品的抢购活动已经结束了----");}//先验证签名String hashKey="KEY_"+userId+id;String s = stringRedisTemplate.opsForValue().get(hashKey);if(s==null){throw new RuntimeException("没有携带验证签名,请求不合法");}if(!s.equals(md5)){throw new RuntimeException("当前请求数据不合法,请稍后再试!");}//根据商品id效验ku库存Stock stock = checkStock(id);//扣除库存updateSale(stock);//创建订单Integer orderId = createOrder(stock);return orderId;}

7. 测试

在这里插入图片描述


六. 单用户限制频率

  • 用redis给每个用户做访问统计,甚至是带上商品id,对单个商品做访问统计,这都是可行的。

  • 我们先实现一个对用户的访问频率限制,我们在用户申请下单时,检查用户的访问次数,超过访问次数,则不让他下单!

在这里插入图片描述

1.UserService接口

package com.xizi.miaosha.service;public interface UserService {//向redis中写入用户访问次数int saveUserCount(Integer userId);//判断单位时间调用次数boolean getUserCount(Integer userId);
}

2.UserServiceImpl 实现类

package com.xizi.miaosha.service.impl;import com.xizi.miaosha.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Slf4j
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;//根据不同用户id生成调用次数的key@Overridepublic int saveUserCount(Integer userId) {String limitKey="LIMIT"+"_"+userId;//获取redis中指定key的调用次数String limitNum = stringRedisTemplate.opsForValue().get(limitKey);int limit=-1;if(limitNum==null){//第一次调用放入redis中设置为0stringRedisTemplate.opsForValue().set(limitKey, "0",3600, TimeUnit.SECONDS);}else{//不是第一次调用每次+1limit= Integer.parseInt(limitNum) + 1;stringRedisTemplate.opsForValue().set(limitKey, String.valueOf(limit),3600, TimeUnit.SECONDS);}return limit; //返回调用次数}@Overridepublic boolean getUserCount(Integer userId) {String limitKey="LIMIT"+"_"+userId;String limitNum = stringRedisTemplate.opsForValue().get(limitKey);if(limitKey==null){log.error("该用户没有申请验证值记录,异常");return  true;}return Integer.parseInt(limitNum)>10;}
}

3.Controler创建接口

    //开发秒杀方法  使用乐观锁防止超卖+令牌桶算法限流+md5加密(id+userId)+单用户次数调用频率@RequestMapping(value = "/killtokenmd5limit",method = RequestMethod.GET)public  String killtokenmd5limit(Integer id,Integer userId,String md5){System.out.println("秒杀商品的id: "+id);//加入令牌桶的限流措施if(!rateLimiter.tryAcquire(2,TimeUnit.SECONDS )){log.info("抛弃的请求:抢购失败,当前秒杀活动过于火爆,请重试");return "抢购失败,当前秒杀活动过于火爆,请重试";}try {//加入单用户int count = userService.saveUserCount(userId);log.info("用户截止该次访问次数为:[{}]",count);//进行判断boolean userCount = userService.getUserCount(id);if(userCount){log.info("购买失败,超过频率限制!");return "购买失败,超过频率限制!";}//根据秒杀的商品id 去调用秒杀业务Integer orderId = orderService.kill(id,userId,md5);System.out.println("秒杀成功,订单id为:"+orderId);return "秒杀成功,订单id为:"+orderId;} catch (Exception e) {e.printStackTrace();return e.getMessage();}}

4.Jmeter压力测试

在这里插入图片描述

七. GitHub源码

点击下载源码进行参考

https://github.com/Y960303802/SpringBoot-Simple-


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部