1. 秒杀系统

1.1 秒杀场景

  • 电商抢购限量商品
  • 售卖明星演唱会门票
  • 火车票抢座12306

1.2 为什么要做秒杀系统

如果你的项目流量非常小,完全不用担心并发的购买需求,那么做这样一个系统意义并不大。

但如果你的系统要像12306那样接收高并发访问和下单的考验,那么你就需要一套完整的流程保护措施,来保证你系统在用户流量高峰期不会挂了。

  • 严格防止超卖:库存100件,你卖了120件,等着辞职吧
  • 防止黑产:防止不怀好意的人群通过各种技术手段把你本该下发给群众的利益全收入囊中
  • 保证用户体验:高并发下,别网页打不开了,支付不成功了,购物车进不去了,地址改不了了。这个问题非常之大,涉及到各种技术,也不是一下子就能讲完的,甚至根本就讲不完。

1.3 保护措施有哪些

  • 乐观锁防止超卖 -----核心基础
  • 令牌桶限流
  • Redis缓存
  • 消息队列异步处理订单

2. 防止超卖

毕竟,你网页可以卡住最多是大家没参与到活动,上网口吐芬芳,骂你一波。但是你要是卖多了,本该拿到商品的用户可就不乐意了
轻则投诉你,重则找漏洞起诉赔偿。让你吃不了兜着走。

2.1 数据库表

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

2.2 分析业务

2.2.1 Controller

package com. baizhi.controller;
import com.baizhi.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping ("stock")
public class StockController{@Autowired
private OrderService OrderService;
//开发秒杀方法
@GetMapping("kill")public String kill(Integer id){try{system.out.println(“秒杀商品的id = " + id);//根据秒杀商品id 去调用秒杀业务int orderId =orderService.kill(id);return "秒杀成功,订单id为:|"+String.value0f(orderId);}catch(Exception e){e.printStackTrace();return e.getMessage();}    }}

2.2.2 Service

package com.baizhi.service;
public interface OrderService {//用来处理秒杀的下单方法|int kill (Integer id);
}

2.2.3 ServiceImpl

package com.baizhi.service;
import com. baizhi.dao. StockDAO;import com.baizhi.entity.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class OrderServiceImpl implements OrderService{@Autowiredprivate StockDAO stockDAO;@Autowiredprivate OrderDAO orderDAO;@Overridepublic  int kill(Integer id){//根据商品id校验库存Stock stock = stockDAO.checkStock (id);if(stock.getsale().equals(stock.getCount())){throw new RuntimeException("库存不足!!!");}else{//扣除库存stock.setsale (stock.getSale()+1);stockDAO.updatesale(stock);//创建订单Order order = new Order();order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());orderDAO.createorder(order);return order.getId();}return 0;
}

useGeneratedKeys=“ture” 使用主键自动生成策略

keyProperty=“id” 把自动生成的主键id的值回赋给Order对象的id属性

2.3 正常测试

在正常测试下没有发现任何问题

2.4 使用Jmeter进行压力测试

2.4.1 Jmeter介绍

Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGl脚本、Java对象、数据库、FTP服务器,等等。JMeter可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。

2.4.2 安装Jmeter

#1.下载jmeter
https:/ ljmeter.apache.org/download_jmeter.cgi
下载地址:https: //mirror.bit.edu.cn/apache//jmeter/binaries/apache-jmeter-5.2.1.tgz
#2.解压缩
backups
---用来对压力测试进行备份目录
bin
---Jmeter核心执行脚本文件
docs
-―-官方文档和案例
extras---额外的扩展lib---第三方依赖库licenses ---说明
printable_docs ---格式化文档
#3.安装Jmeter
日.要求:必须事先安装jdk环境1.配置jmeter环境变量
export JMETER_HOME=/Users/chenyannan/dev/apache-jmeter-5.2
export PATH=$SCALA_HOME/bin:$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH:$JMETER._HONE/bin2.是配置生效
source ~/ .bash_profile3.测试jemeter

2.4.3 Jmeter使用

Don’t use GUl mode for load testing !, only for Test creation and Test debugging.For load testing,

use CLl Mode (was NON GUI):

jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]

& increase Java Heap to meet your test requirements:
Modify current env variable HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m" in the jmeter batch file

Check : https://jmeter.apache.org/usermanual/best-practices.html

2.4.4 Jmeter压力测试

jmeter -n -t [jmx file](jmx压力测试文件)-l [results file](结果输出的文件) -e -o [Path to web report folder](生成html版压力测试报告)

synchronized线程如果比事务范围小, 释放锁后 ,事务没有结束,数据没有提交到数据库,库存没有改,第二个线程如果执行比较快,跑过了第一个,将数据提交后,第一个才提交事务,这样就可能出现了超卖。

注意:最好不要在业务方法上加同步代码块。

在控制器调用方法处加Synchronized,可以保证不影响事务的提交,保证线程的执行范围比事务的执行范围大

2.5 乐观锁解决超卖问题

CAS+Version

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

2.5.1 校验库存方法

//校验库存
private stock checkStock (Integer id){Stock stock = stockDAO.checkStock (id);
if(stock.getsale().equals (stock.getCount ()))
{thrownew RuntimeException("库存不足!!!");
}return stock;
}

2.5.2 更新库存的方法改造

//扣除库存
private void updateSale(Stock stock){//在sq1层面完成销量的+1 和版本号的+1 并且根据商品id和版本号同时查询更新的商品stockDAO.updateSale(stock);
}
<!--根据商品id扣除库存-->
<update id= "updateSale" parameterType=" Stock ">update stock setsale=sale+1,version=version+1whereid=#{id}andversion =#{version}
</update>

2.5.3 创建订单

    //创建订单private Integer createOrder (Stock stock){Order order =new Order();order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());orderDAO.createOrder(order);return order.getId();}
<!--创建订单-->
<insert id="createOrder" parameterType= "order" useGeneratedKeys= "true" keyProperty="id">insert into stock_order values(#{id},#{sid},#{name},#{createDate})
</insert>

2.5.4 完整的业务方法与Mapper.xml

  • Service方法
package com.baizhi.service;
import com. baizhi.dao. StockDAO;import com.baizhi.entity.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class OrderServiceImpl implements OrderService{@Autowiredprivate StockDAO stockDAO;@Autowiredprivate OrderDAO orderDAO;
@override
public int kill(Integer id){//校验库存stockstock = checkStock(id) ;//更新库存updatesale(stock);//创建订单return createOrder(stock);}//校验库存private stock checkStock (Integer id){Stock stock = stockDAO.checkStock (id);if(stock.getsale().equals (stock.getCount ())){thrownew RuntimeException("库存不足!!!");}return stock;}//扣除库存private void updatesale (Stock stock){//在sql层面完成销量的+1 和版本号的+1//并且根据商品id和版本号同时查询更新的商品int updateRows = stockDAO.updatesale(stock) ;if (updateRows==0){throw new RuntimeException("请购失败,请重试!!!" );}}//创建订单private Integer createOrder (Stock stock){Order order =new Order();order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());orderDAO.createOrder(order);return order.getId();}
  • StockDaoMapper.xml
<!--根据秒杀商品id查询库存-->
<select id="checkStock" parameterType="int " resultType="Stock ">
select id ,name , count,sale, version from stock
where id =#{id}
</select>
<!--根据商品id扣除库存-->
<update id= "updateSale" parameterType=" Stock ">update stock setsale=sale+1,version=version+1whereid=#{id}andversion =#{version}
</update>
  • OrderDaoMapper.xml
<!--创建订单-->
<insert id="createOrder" parameterType= "order" useGeneratedKeys= "true" keyProperty="id">insert into stock_order values(#{id},#{sid},#{name},#{createDate})
</insert>

3.接口限流

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

3.1 接口限流

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

3.2 如何实现接口限流

常用的限流算法有 令牌桶漏桶(漏斗算法),而Google开源项目Guave中的Raeuiniter使用的就是令牌桶控制算法。

在开发高并发系统时有三把利器用来保护系统: 缓存、降级 和 限流

  • 缓存 :缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级︰降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
  • 限流︰限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

3.3 令牌桶算法和漏斗算法

  • 漏斗算法:漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
  • 令牌桶算法:最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。
  • 令牌桶算法的扩展性比漏斗算法要强很多,漏桶算法比较粗暴,令牌桶算法可以设置超时时间

3.4 使用令牌桶算法实现乐观锁 + 限流

3.4.1 项目中引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
3.4.2 令牌桶算法的基本使用
package com. baizhi.controller;
import com.baizhi.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping ("stock ")
@Slf4j
public class StockController{@Autowired
private OrderService OrderService;
//创建令牌桶实例private RateLimiter rateLimiter = RateLimiter.create(30);
/*@GetMapping("sale")public String sale(Integer id){//处理策略://1.没有获取到token请求 就一直阻塞 直到获取到token令牌//log.info("等待时间:"+rateLimiter.acquire());//2.设置一个等待时间,如果等待时间内获取到了令牌,则处理业务//  如果等待时间内没有获取到了令牌,则抛弃业务// 如果5秒内能拿到if(rateLimiter.tryAcquire(5,TimeUnit.SECONDS)){sout("当前请求被限流,直接抛弃,无法调用后续秒杀逻辑......");return "抢购失败";}sout("处理业务.............")return "抢购成功";}*/
//开发秒杀方法,使用乐观锁防止超卖
@GetMapping("kill")public String kill(Integer id){try{system.out.println(“秒杀商品的id = " + id);//根据秒杀商品id 去调用秒杀业务int orderId =orderService.kill(id);return "秒杀成功,订单id为:|"+String.value0f(orderId);}catch(Exception e){e.printStackTrace();return e.getMessage();}    }//开发秒杀方法,使用乐观锁防止超卖 + 令牌桶限流@GetMapping("killtoken")public String killtoken(Integer id){//加入令牌桶的限流措施if(!Ratelimiter.tryAcquire(3,TimeUnit.SECONDS))//超时时间:3s{log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");return "抢购失败,当前秒杀活动过于火爆,请重试";}try{system.out.println(“秒杀商品的id = " + id);//根据秒杀商品id 去调用秒杀业务int orderId =orderService.kill(id);return "秒杀成功,订单id为:|"+String.value0f(orderId);}catch(Exception e){e.printStackTrace();return e.getMessage();}    }                         }

注意:加了该限流措施后,在并发抢购的情况下,是不可能百分之百全部卖掉的,

​ 可能是卖掉大部分商品,但是这也并不算是缺点,因为,在正式的电商场景中,

​ 我们需要留一部分库存去解决部分产品质量不合格用户退换问题;

4.隐藏秒杀接口

之前设计的系统存在的一些问题:

1.我们应该在一定时间内执行秒杀处理,不能在任意时间都接受秒杀请求,如何加入时间验证?

2.对于稍微懂点电脑的,又会动歪脑筋的人来说,通过抓包的方式获取我们的接口地址,然后通过脚本进行抢购怎么办?

3.秒杀开始之后如何限制单个用户的请求频率,即单位时间内访问次数?

这个章节主要讲解秒杀系统中,关于抢购(下单)接口相关的单用户防刷措施,主要说明几块内容:

  • 限时抢购
  • 抢购接口隐藏
  • 单用户限制频率(单位时间内限制访问次数)

4.1 限时抢购的实现

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

4.1.1 启动Redis服务

4.1.2 引入Redis依赖

<!--spring boot stater data redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

4.1.3 配置redis

spring.redis.database=0
spring.redis.port=6379
spring.redis.host=localhost

4.1.4 代码

package com.baizhi.service;
import com. baizhi.dao. StockDAO;import com.baizhi.entity.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class OrderServiceImpl implements OrderService{@Autowiredprivate StockDAO stockDAO;@Autowiredprivate OrderDAO orderDAO;//注入stringRedisTemplate@Autowiredprivate stringRedisTemplate stringRedisTemplate;
@override
public int kill(Integer id){//校验redis中的秒杀商品是否超时if(!stringRedisTemplate.hasKey("kill"+id))//常量key+商品id 作为键{throw new RuntimeException("当前商品的抢购活动已经结束啦---");}//校验库存stockstock = checkStock(id) ;//更新库存updatesale(stock);//创建订单return createOrder(stock);}//校验库存private stock checkStock (Integer id){Stock stock = stockDAO.checkStock (id);if(stock.getsale().equals (stock.getCount ())){thrownew RuntimeException("库存不足!!!");}return stock;}//扣除库存private void updatesale (Stock stock){//在sql层面完成销量的+1 和版本号的+1//并且根据商品id和版本号同时查询更新的商品int updateRows = stockDAO.updatesale(stock) ;if (updateRows==0){throw new RuntimeException("请购失败,请重试!!!" );}}//创建订单private Integer createOrder (Stock stock){Order order =new Order();order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());orderDAO.createOrder(order);return order.getId();}

4.1.5 Jmter测试

4.1.6 设置键和超时时间

EX 15 超时时间 15s

4.2 抢购接口隐藏

对于稍微懂点电脑的,又会动歪脑筋的人来说,点击F12打开浏览器的控制台,就能在点击抢购按钮后,获取我们抢购接口的链接。(手机APP等其他客户端可以抓包来拿到)一旦坏蛋拿到了抢购的链接,只要稍微写点爬虫代码,模拟一个抢购请求,就可以不通过点击下单按钮,直接在代码中请求我们的接口,完成下单。所以就有了成千上万的藕羊毛军团,写一些脚本抢购各种秒杀商品。

他们只需要在抢购时刻的000毫秒,开始不间断发起大量请求,觉得比大家在APP上点抢购按钮要快,毕竟人的速度又极限,更别说APP说不定还要经过几层前端验证才会真正发出请求。

所以我们需要将抢购接口进行隐藏,抢购接口隐藏(接口加盐)的具体做法︰

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

最后一句话写错了,是根据用户id和商品id 加上随机盐生成的md5存入redis并且设置超时时间,每次抢购都只是在很短的时间内有效,超过了时间,需要重新生成md5,重新发送请求。

4.2.1 库表结构

4.2.2 控制器代码

///生成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 ;        }
}

4.2.3 ServiceImpl

package com.baizhi.service;
import com. baizhi.dao. StockDAO;import com.baizhi.entity.Stock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@Transactional
public class OrderServiceImpl implements OrderService{@Autowiredprivate StockDAO stockDAO;@Autowiredprivate OrderDAO orderDAO;//注入stringRedisTemplate@Autowiredprivate stringRedisTemplate stringRedisTemplate;@overridepublic int kill(Integer id,Integer userid,String md5){//校验redis中的秒杀商品是否超时//  if(!stringRedisTemplate.hasKey("kill"+id))//常量key+商品id 作为键//  {//       throw new RuntimeException("当前商品的抢购活动已经结束啦---");//  }if(md5==null){}//先验证签名String  hashKey = "KEY_"+userid+"_"+id;if(!String s = StringRedisTemplate.opsForValue().get(hashKey).equals(md5)){throw new RuntimeException("当前请求数据不合法,请稍后再试");}//校验库存stockstock = checkStock(id) ;//更新库存updatesale(stock);//创建订单return createOrder(stock);}@overrideprivate String getMd5(Integer id,Integer userid){//检验用户的合法性User user = userDAO.findById(userid);if(user==null)throw new RuntimeException("用户信息不存在! ");log.info("用户信息:[{}]", user.toString () );//检验商品的合法性Stock stock = stockDAO.checkStock(id);if(stock==null) throw new RuntimeException("商品信息不合法! ");log .info("商品信息:[{}]" , stock.toString() );//生成hashkeyString hashKey = "KEY_"+userid+"_"+id;//生成md5//这里!Q*jS#是一个盐 随机生成String key = DigestUtils.md5DigestAsHex((userid+id+"!Q*jS#" ).getBytes());stringRedisTemplate.opsForValue().set(hashKey,key,3600,TimeUnit.SECONDS);log.info("Redis写入:[{}][{}]",hashKey, key) ;return key ;}@overridepublic int kill(Integer id){//校验redis中的秒杀商品是否超时if(!stringRedisTemplate.hasKey("kill"+id))//常量key+商品id 作为键{throw new RuntimeException("当前商品的抢购活动已经结束啦---");}//校验库存stockstock = checkStock(id) ;//更新库存updatesale(stock);//创建订单return createOrder(stock);}//校验库存private stock checkStock (Integer id){Stock stock = stockDAO.checkStock (id);if(stock.getsale().equals (stock.getCount ())){thrownew RuntimeException("库存不足!!!");}return stock;}//扣除库存private void updatesale (Stock stock){//在sql层面完成销量的+1 和版本号的+1//并且根据商品id和版本号同时查询更新的商品int updateRows = stockDAO.updatesale(stock) ;if (updateRows==0){throw new RuntimeException("请购失败,请重试!!!" );}}//创建订单private Integer createOrder (Stock stock){Order order =new Order();order.setSid(stock.getId()).setName(stock.getName()).setCreateDate(new Date());orderDAO.createOrder(order);return order.getId(); }

4.2.4 业务层代码

@override
public String getMd5(Integer id,Integer userid) {//检验用户的合法性User user = userDAO.findById(userid);if(user==null)throw new RuntimeException("用户信息不存在! ");log.info("用户信息:[{}]", user.toString () );//检验商品的合法性Stock stock = stockDAO.checkStock(id);if(stock==null) throw new RuntimeException("商品信息不合法! ");log .info("商品信息:[{}]" , stock.toString() );//生成hashkeyString hashKey = "KEY_"+userid+"_"+id;//生成md5//这里!Q*jS#是一个盐 随机生成String key = DigestUtils.md5DigestAsHex((userid+id+"!Q*jS#" ).getBytes());stringRedisTemplate.opsForValue().set(hashKey,key,3600,TimeUnit.SECONDS);log.info("Redis写入:[{}][{}]",hashKey, key) ;return key ;
}

4.2.5 Dao代码和Entity

@Data
public class User {private Integer id;private String name;private String password;
}
@Mapper
public interface UserDAO{User findById(Integer id);
}
<mapper namespace = "com.baizhi.dao.UserDAO"><!--根据id查询用户方法-->
<select id="findById" parameterType="Integer" resultType="User">select id,name,password from user where id=#{id}
</select>
</mapper>

4.2.6 数据库添加用户记录

4.2.7 查看商品信息

4.2.8 启动项目访问生成md5接口

4.2.9 携带验证码下单即可

Controller代码
//开发一个秒杀方法乐观锁防止超卖+令牌桶算法限流
GetMapping ("killtokenmd5")
public String killtoken( Integer id,Integer userid, String md5) {System.out.println("秒杀商品的id = " + id);//加入令牌桶的限流措施if (!rateLimiter.tryAcquire(3,TimeUnit.SECONDS)) {log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");return"抢购失败,当前秒杀活动过于火爆,请重试!";}try {//根据秒杀商品id 去调用秒杀业务int orderId = orderService.kil1(id , userid , md5);return"秒杀成功,订单id为:" + String. valueof(orderId);} catch (Exception e) {e.printStackTrace();return e.getMessage();
}
Service代码
@override
public int kill(Integer id,Integer userid,String md5){//校验redis中秒杀商品是否超时////if( !stringRedisTemplate.hasKey ( "ki11"+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("当前请求数据不合法,请稍后再试! ");//校验库存Stock stock = checkStock(id);//更新库存updateSale(stock);//创建订单return createOrder( stock);
}

4.3 单用户接口限制频率

假设我们做好了接口隐藏,但是像我上面说的,总有无聊的人会写一个复杂的脚本,先请求hash(md5)值,再立刻请求购买,如果你的app下单按钮做的很差,大家都要开抢后0.5秒才能请求成功,那可能会让脚本依然能够在大家前面抢购成功。
我们需要在做一个额外的措施,来限制单个用户的抢购频率。
其实很简单的就能想到用redis给每个用户做访问统计,甚至是带上商品id,对单个商品做访问统计,这都是可行的。我们先实现一个对用户的访问频率限制,我们在用户申请下单时,检查用户的访问次数,超过访问次数,则不让他下单!

  • 具体流程:

4.3.1 Controller代码

//开发一个秒杀方法乐观锁防止超卖+令牌桶算法限流
@GetMapping( "killtokenmd5limit" )
public String killtokenlimit(Integer id, Integer userid,String md5)
{//加入令牌桶的限流措施if(!rateLimiter.tryAcquire(3,TimeUnit.SECONDS)){    log.info("抛弃请求:抢购失败,当前秒杀活动过于火爆,请重试");return"抢购失败,当前秒杀活动过于火爆,请重试! ";}try {      //加入单用户限制调用频率int count = userService.saveUserCount(userid);log.info("用户截至该次的访问次数为:[{}]", count) ;boolean isBanned = userService.getUserCount(userid);if(isBanned){log.info("购买失败,超过频率限制!");return"购买失败,超过频率限制!";}//根据秒杀商品id 去调用秒杀业务int orderId = orderService.kil1(id,userid,md5) ;return"秒杀成功,订单id为: " + String.valueOf(orderId);}catch (Exception e) {e.printStackTrace();return e.getMessage();}
}

4.3.2 Service接口及实现

接口
public interface UserService {//向redis中写入用户访问次数int saveUsercount(Integer userId);//判断单位时间调用次数boolean getUserCount(Integer userId);
}
实现
@Service
@Transactional
@Slf4j
public class UserServiceImpl implements UserService {@Autowiredprivate stringRedisTemplate stringRedisremplate;@overridepublic int saveUserCount (Integer userId){//根据不同用户id生成调用次数的keystring limitKey = "LIMIT"+ "_" + userId;//获取redis中指定key的调用次数string limitNum = stringRedisTemplate.opsForValue().get (limitKey);int limit =-1;if(limitNum -= nul1){//第─次调用放入redis中设置为0stringRedisTemplate.opsForValue().set(limitKey,"0",3600,TimeUnit.SECONDS);} else {//不是第一次调用每次+1limit = Integer.parseInt(limitNum)+l;stringRedisTemplate.opsForValue().set(limitKey,String.valueOf(limit),3600,TimeUnit.SECONDS);return limit; //返回调用次数}@overridepublic boolean getUserCount (Integer userId){//根据userid对应key获取调用次数String limitKey = "LIMIT"+ "_"+ userId;//根据用户调用次数的key获取redis中调用次数string limitNum = stringRedisTemplate.opsForValue().get (limitKey);if(limitNum == null){//为空直接抛弃说明key出现异常log.error( "该用户没有访问申请验证值记录,疑似异常");return true;return Integer.parseInt(limitNum)> 10; //false代表没有超过true代表超过}}

4.3.3测试调用

总结:

乐观锁防止超卖+令牌桶限流+md5签名(hash接口隐藏,防止脚本跨过前端秒杀按钮直接发起秒杀请求)+单用户限制频率
1.在秒杀开始前,我们会把相应的秒杀请求(kill+商品id)存入Redis并设置超时时间,秒杀开启后,当我们进入某个秒杀页面的时候点击秒杀按钮的时候发起请求,服务器端会根据用户id和商品id还有随机盐生成md5(签名),Key=“KEY_”+userid+"_"+id;
2. 存入Redis的同时返回给用户界面,当用户想要秒杀某个商品需要输入该md5值才能继续下一步,该请求一定要先从令牌桶获取令牌rateLimiter.tryAcquire(3, TimeUnit.SECONDS),获取到令牌,才能执行下一步,
3.获取到令牌后,先校验用户请求的次数是否超过限制,如果没有超过限制,先校验redis中存的秒杀请求是否过期,再验证签名(也是从Redis根据Key中获取),(如果要求每个人只能限购一个就验证下是否重复下单),然后校验库存,更新库存,创建订单(创建订单入库时,服务端创建id,使用insert ignore语句,如果受影响行数为0则说明已经下过单),需要注意的是更新库存使用的是乐观锁,在sql层面完成库存的-1 和 版本号的+1 ,如果返回的受影响行数>0则扣减库存成功,再校验库存。
insert ignore表示,如果中已经存在相同的记录,则忽略当前新数据;

为什么更新库存后还要校验库存?

因为并发环境下,比如有AB两个用户同时抢购了该商品,他们的秒杀请求同时通过了层层关卡,通过了重复下单校验,也同时通过了库存校验,更新库存是没问题,保证不会超卖没问题,但是可能会存在,有两件商品,A抢购成功了,B由于乐观锁版本号原因,没能抢购成功,库存还剩下一件没卖出去,这就有问题了。

如何解决呢?

我们需要在更新库存操作的后边再加一个查询库存操作,如果库存还有,且他还没有该商品的订单,让他继续抢购。

 <update id="updateSale" parameterType="Stock">update stock setcout=count-1,version=version+1whereid =#{id}andversion = #{version}</update>

说明:本文非原创,是看着B站编程不良人的视频记得笔记,

如果感觉对你个人有用的话,不要忘了点赞三连哟!

秒杀笔记(乐观锁+令牌桶+Redis缓存)相关推荐

  1. Shiro框架学习笔记、整合Springboot、redis缓存

    本笔记基于B站UP主不良人编程 目录 1.权限的管理 1.1什么是权限管理 1.2什么是身份认证 1.3什么是授权 2.什么是Shiro 3.Shiro的核心架构 3.1 S核心内容 4.shiro中 ...

  2. Springboot秒杀系统(乐观锁+RateLimiter令牌+Redis缓存)

    本文主要是利用springboot,实现一个单机版秒杀demo,通过单机版实现,可以对基本并发秒杀的知识有一定的了解. 首先先提供秒杀业务实现类: /*** spring 注解加在实现类*/ @Ser ...

  3. Redis缓存详解(黑马-未完结)

    文章目录 1.场景引入 2.NoSQL数据库 2.1NoSQL简介 2.2NoSQL的适用场景 2.3NoSQL不适用的场景 2.4NoSQL数据库的意义 3.SQL与NoSQL的区别 4.Redis ...

  4. MySQL建表添加乐观锁字段_Java秒杀系统优化-Redis缓存-分布式session-RabbitMQ异步下单-页面静态化...

    Java秒杀系统优化-Redis缓存-分布式session-RabbitMQ异步下单-页面静态化 项目介绍 基于SpringBoot+Mybatis搭建的秒杀系统,并且针对高并发场景进行了优化,保证线 ...

  5. Redis事务和锁机制(乐观锁+秒杀)

    目录 命令 组队Multi错误(命令此时不会真正执行): 执行exec错误: 事务冲突 解决方案 悲观锁: 乐观锁: 场景: 演示乐观锁,watch key监控 Redis事务总结: 秒杀案例 ab测 ...

  6. 【Redis实现秒杀业务③】超卖问题之乐观锁具体实现

    文章目录 版本号法 CAS算法 图解 代码实现 版本号法 版本号机制实现的方式常用的也有两种: 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式.何谓数据版本?即为数据增加一 ...

  7. 【秒杀系统】零基础上手秒杀系统(二):令牌桶限流 + 再谈超卖

    前言 本文是秒杀系统的第二篇,通过实际代码讲解,帮助你快速的了解秒杀系统的关键点,上手实际项目. 本篇主要讲解接口限流措施,接口限流其实定义也非常广,接口限流本身也是系统安全防护的一种措施,暂时列举这 ...

  8. 十三水算法php_基于PHP+Redis令牌桶限流

    一 .场景描述 在开发接口服务器的过程中,为了防止客户端对于接口的滥用,保护服务器的资源, 通常来说我们会对于服务器上的各种接口进行调用次数的限制.比如对于某个 用户,他在一个时间段(interval ...

  9. 阿里蚂蚁金服中间件(Java 4轮面试题含答案):Redis缓存+线程锁+微服务等

    第一轮 说说HaspMap底层原理?再说说它跟HaspTable和ConcurrentHashMap他们之间的相同点和不同点? 讲讲jdk1.7和1.8的区别? 几种内置的线程池 MySQL事务隔离级 ...

  10. php限制接口访问次数_令牌桶限流思路分享(PHP+Redis实现机制)

    一 .场景描述 在开发接口服务器的过程中,为了防止客户端对于接口的滥用,保护服务器的资源, 通常来说我们会对于服务器上的各种接口进行调用次数的限制.比如对于某个 用户,他在一个时间段(interval ...

最新文章

  1. WIN7实现多用户远程桌面
  2. 如何低成本实现Flutter富文本,看这一篇就够了!
  3. mysql2005卸载步骤,二次安装mysql步骤
  4. 人工智能是 6G 诞生的关键!| 极客头条
  5. String、Object、包装类的常用方法以及注意点
  6. Git-第一篇认识git,核心对象,常用命令
  7. RAPIDXML 中文手册,根据官方文档完整翻译!
  8. window.dialogArguments 使用问题
  9. 吴军,阅读与写作,11,写景:如何寓情于景?
  10. Latex自定义图表序号
  11. Python 实现字节转换函数
  12. 【prometheus+alertmanager告警管理】
  13. oracle函数 isex,求一条sql话语,按条件查询过期的客户信息
  14. 【机器学习】金融风控评分卡建模全流程!
  15. R语言绘图:条形图——barplot
  16. 如何防止表单重复提交(后端)
  17. 哔哩哔哩2020校园招聘前端笔试卷(一)
  18. 自动驾驶决策控制及运动规划史上最详细最接地气综述
  19. 实现LED灯的点亮与闪烁和跑马灯
  20. 用C++画心形曲线: 送给母亲的心(母亲节2020.5.10)

热门文章

  1. 中小企业OA系统如何选型?OA软件选型必看技巧
  2. 数独题 HDU - 1426
  3. 博客做外链(可以发布外链的博客有哪些平台)
  4. 谷歌,微软,阿里,美团实习生面经
  5. 软件测试分类、分级与软件缺陷管理
  6. java subject类的作用_RxJava2 系列-3:使用 Subject
  7. iOS14:AirPods Auto Switching
  8. 多余的回车键(Enter)
  9. MyBayis插件-逆向工程
  10. 田忌赛马java代码算法,AcWing 1489. 田忌赛马——Java版代码