A:查询模块(查询库存)并发查询,库存数存在缓存中,(商品信息和图片信息等)静态化处理(生成静态页)和库存剩余数量缓存化处理。
B: 下订单模块(秒杀关键部分),队列控制异步化处理,首先判断这个队列是否已满, 如果没满就将请求放入队列中排队,队列满以后的所有请求直接返回秒杀失败。
C: 支付模块,异步付款,等待付款成功结果。(付款成功更新库存,也可下单的时候扣库存)。
读多写少 (一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存)
高并发
限流 (限制同一时间访问的流量)
负载均衡 (单体tomcat并发200完美胜任,突破五,六百就力不从心)
缓存 (预先把秒杀商品加载进内存)
异步 (将同步的并发请求转换为异步,多线程处理)
队列 (使用redis队列,因为pop操作是原子的,即使有很多用户同时到达,也是依次执行)
(1).设计思路
超买超卖问题的解决。
**将请求拦截在系统上游,降低下游压力: ** 秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时
**充分利用缓存: **利用缓存可极大提高系统读写速度。
消息队列: 消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。
秒杀活动开始前,其实就有很多用户访问该页面了。如果这个页面的一些资源,比如 CSS、JS、图片、商品详情等,都访问后端服务器,甚至 DB 的话,服务肯定会出现不可用的情况。
解决方案: 所以要创建静态页面让这个页面整体进行静态化,并将页面静态化之后的页面分发到 CDN(类似于资源服务器) 边缘节点上,起到压力分散的作用
生成的静态页面会遇到的问题: 由于我们以后开发的系统肯定不是给自己用的,用户可能处于不同的时区,他们的当前系统时间也是不同的,所以我们写一个通用的时间规范:就是当前服务器的时间;
seckill_order
中联合主键的配置实现。
动态资源与静态资源进行分离,获取静态资源时不走Tomcat
限制某个IP同一时间段的访问次数,即针对某一个IP,限制单位时间内发起请求数量。
http {
limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
limit_req_zone $server_name zone=perserver:10m rate=10r/s;
...
server {
...
limit_req zone=perip burst=5 nodelay; #漏桶数为5个.也就是队列数.nodelay:不启用延迟.
limit_req zone=perserver burst=10; #限制nginx的处理速率为每秒10个
}
# 静态资源
server {
#侦听端口
listen 80;
#当前虚拟机所配置的域名信息[所有该域名访问都遵循该配置规则]
server_name www.seckill.com;
#所有以.jpg,.png。。。都遵循该配置路由规则
location ~ \.(jpg|png|gif|js|css)$ {
#都直接到D盘的images目录找文件
root D:/images;
}
}
}
原子性(atomicity):一个事务是一个不可分割的最小工作单位,要么都成功要么都失败
Redis所有单个命令的执行都是原子性的
当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。
解决方案:
数据库
update seckill set num=num-1 where num>0 and goodsId=1
数据库查询、更新的时候有用到行级锁,是可以保证更新操作的原子性的。数据库性能较差,不建议使用
```
Redis分布式锁
用redis来做一个分布式锁,reids->setnx(‘lock’, 1) 设置一个锁,程序执行完成再解锁。锁定的过程,不利于并发执行,所有线程都在等待锁解开,不建议使用。
```
消息队列
将订单请求全部放入消息队列,然后另外一个后台程序监听消息一个个处理队列中的订单请求。
并发不受影响,但是用户等待的时间较长,进入队列的订单也会很多,体验上不好,不建议使用。
```
redis递减
通过 redis->incrby('product', -1) 得到递减之后的库存数。
redis的incr和decr 可以实现原子性的递增递减
```
```java
// 根据抢购的商品id
Seckill seckill = (Seckill) redisTemplate.boundHashOps("seckill").get(goodsId);
if (seckill.getStockCount() < 0) {
// 商品已售完
throw new SeckillException("商品已售完");
}
// 减少抢购商品的库存信息
Long count = redisTemplate.boundHashOps("seckill_count").increment(goodsId,-1);
// 更新库存信息
seckill.setStockCount(count);
if (count <= 0) {
// 库存不足
// 更新数据库库存数据
seckill.setStockCount(count);
seckillMapper.update(seckill);
// 秒杀结束,把订单信息更新到MySQL中
throw new SeckillException("库存不足,谢谢参与!");
} else {
// 更新库存信息
redisTemplate.boundHashOps("seckill").put(goodsId,seckill);
// 存储订单信息,暂时存储到redis中
}
```
活动周期短,瞬间流量大(高并发),大量的人短期涌入服务器抢购,但是数量有限,最终只有少数人能成功下单。
解决方案:
可以使用限流限制过多请求访问系统
’栈’:从链表的头部添加元素,先进后出
’队列’:从链表的尾部添加元素,先进先出
// 存入队列, 构建用户抢单信息
SeckillStatus seckillStatus = new SeckillStatus(userPhone, new Date(), 1, seckillId);
// 存储用户排队信息(先进先出)
redisTemplate.boundListOps("seckillOrderStatus").leftPush(seckillStatus);
// 获取队列排队中的用户
SeckillStatus seckillStatus = (SeckillStatus)redisTemplate.boundListOps("seckillOrderStatus").rightPop();
不要做多集群的数据同步,而是用散列,每个集群的数据是独立存在的。
假设,有10个商品,每个商品有1w库存,规划用10个集群,那么每个集群有10个商品,每个商品是1k库存。
每个集群只需要负责把自己的库存卖掉即可,至于说,会不会有用户知道有10个集群,然后每个集群都去抢。
这种情况就不要用程序来处理了,利用运营规则,活动结束后汇总订单的时候再去处理
比如:某个集群用户访问量特别少,那么可以引入一个中控服务,来监控各个集群的库存,然后再做平衡。
垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务"的做法相似,每个微服务使用单独的一个数据库
也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。
秒杀跟普通商品购买是有区别的,所以数据库表设计也不同 进行业务隔离
将这种秒杀数据隔离出来,不要让1%的请求影响到另外的99%,隔离出来后也更方便对这1%的请求做针对性优化
进行表与表之间的分离
垂直切分的优点:
解决业务系统层面的耦合,业务清晰
与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈
1.Ratelimiter的简单使用
常用的两种限流方式
使用漏桶(Leaky Bucket)算法来进行限流
使用令牌桶(Token Bucket)算法来进行限流
漏桶: 类似于队列, 水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率
QPS-->Queries Per Second “每秒查询率” 一台服务器每秒能够相应的查询次数
令牌桶: 系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
项目中的使用
导入pom.xml依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>27.1-jreversion>
dependency>
@Service
public class AccessLimitServiceImpl implements AccessLimitService {
/**
* 每秒钟只发出60个令牌,拿到令牌的请求才可以进入秒杀过程
*/
private RateLimiter seckillRateLimiter = RateLimiter.create(60);
/**
* 尝试从令牌桶中获取令牌
* @return
*/
@Override
public boolean tryAcquireSeckill() {
return seckillRateLimiter.tryAcquire();
}
}
@Autowired
private GuavaRateLimiterService rateLimiterService;
@ResponseBody
@RequestMapping("/ratelimiter")
public Result testRateLimiter(){
if(rateLimiterService.tryAcquire()){
return ResultUtil.success1(1001,"成功获取令牌");
}
return ResultUtil.success1(1002,"未获取到令牌");
}
ava
@Autowired
private GuavaRateLimiterService rateLimiterService;
@ResponseBody
@RequestMapping("/ratelimiter")
public Result testRateLimiter(){
if(rateLimiterService.tryAcquire()){
return ResultUtil.success1(1001,“成功获取令牌”);
}
return ResultUtil.success1(1002,“未获取到令牌”);
}