赞
踩
秒杀项目需要注意的点:
抢购之前的预约通知:点击预约产生token,token会放在用户的浏览器里,无token的用户只是在前端提示商品不足,获取token的用户可以请求后台,将重复请求前端拦截
抢购开始之前暴露接口。被黑客截取,通过脚本参与秒杀:使用网关,通过网关进行相应的限流,如:黑名单(将IP地址、用户ID),重复请求放在Redis集群,将同一个IP的发起采取拒绝考虑Redis的性能瓶颈可以做分片,带宽,统一处理
对没有token的用户:尽快处理前面已经获得token的请求,将商品进行卖光,在网关处直接终结请求,每一个Tomcat可做一到两千的QPS,令牌桶发放完就进入下单阶段
对于下单阶段要最快生成订单,否则会出现超时,可使用Redis。考虑Redis的性能可以使用分片,作用是速度快,订单查询可减少对数据库的冲击,同时订单走队列进行削峰,后端进行消费,入库成功后就可将Redis中的数据删除
出现令牌桶发放超出库存情况采用分布式锁,Redis封装好的分布式锁的方案,针对商品Id加分布式锁,但是如果商品众多,加锁反而会对性能产生影响,对Redis的压力较大
可直接在服务器实例里写好商品数量,在内存里判空,不用走Redis,不用通信,性能较高
使用到微服务采用配置中心,通过配置中心下发每个实例的商品数量,可以后台控制,在抢购开始的时候,通过配置中心下发到每个服务商品数量,当实例将内存中的商品数量消耗完毕,即为卖完了
抢购过程中服务挂掉了,大不了少卖一些,等所有服务卖完,统计订单数量,将剩余库存再次启动,再次售卖
为什么做这个项目?
希望将过去所学的一些知识做一个系统的深入理解。秒杀项目运用场景多,涉及的问题与中间件较为复杂,更有利于对web服务的深入学习。
详细过程?
本项目主要是为了模拟一种高并发的场景,请求到达nginx后首先经由负载轮询策略到达某一台服务器中(后端部署了两台服务器)。为了解决秒杀场景下的入口大流量、瞬时高并发问题。引入了redis作为缓存中间件,主要作用是缓存预热、预减库存等等。引入秒杀令牌与秒杀大闸机制来解决了入口大流量问题。引入线程池技术来解决了浪涌(高并发)问题。
直接由数据库操作库存的sql语句如下所示。依靠MySQL中的排他锁实现
update table_prmo set num = num -1 WHERE id =1001 and num >0
利用redis
的单线程特性预减库存处理秒杀超卖问题!!!
Redis
缓存中;(缓存预热)Redis
中进行预减库存(decrement),当Redis
中的库存不足时,直接返回秒杀失败,否则继续进行第3步;mysql
唯一索引(商品索引)+ 分布式锁
设置热点数据永远不过期。
非分布式的系统中使用Spring提供的事务功能即可。
**分布式事务:**将减库存与生成订单操作组合为一个事务。要么一起成功,要么一起失败。
CAP理论(只能保证 CP、AP)、BASE理论(最终一致性,基本可用性、柔性事务)。
分布式事务的两个协议以及几种解决方案:
seata
分布式事务控制组件。
秒杀令牌(token)加秒杀大闸限制入口流量。线程池技术限制瞬时并发数。验证码做防刷功能。
封IP,nginx
中有一个设置,单个IP访问频率和次数多了之后有一个拉黑操作。
分布式锁。redission
客户端实现分布式锁。
decrement API减库存,increment API回增库存。以上的指令都是原子性的。
典型的缓存雪崩问题,给缓存中的数据的过期时间加随机数。
组redis
集群,主从模式、哨兵模式、集群模式。
主从模式中:如果主机宕机,使用slave of no one 断开主从关系并且把从机升级为主机。
哨兵模式中:自动监控master / slave的运行状态,基本原理是:心跳机制+投票裁决。
每个sentinel会向其它sentinel、master、slave定时发送消息(哨兵定期给主或者从和slave发送ping包(IP:port),正常则响应pong,ping和pong就叫心跳机制),以确认对方是否“活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的“主观认为宕机” Subjective Down,简称SDOWN)。
若master被判断死亡之后,通过选举算法,从剩下的slave节点中选一台升级为master。并自动修改相关配置。
那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。
1、nginx做一个动静分离以及负载均衡
2、redis缓存预热、预减库存
3、MQ异步下单
token+redis
解决分布式会话问题。
Token是服务端生成的一串字符串,作为客户端进行请求的一个令牌,当第一次登录后,服务器生成一个userToken
便将此Token返回给客户端,存入cookie中保存,以后客户端只需带上这个userToken
前来请求数据即可,无需再次带上用户名和密码。二次登录时,只需要去redis
中获取对应token的value,验证用户信息即可。
- // 用户第一次登录时,经过相关信息的验证后将对应的登录信息以及凭证(token)存入reids中
- String uuid = UUID.rondom().toString();
- redisTemplate.opsForValue().set(uuid, userModel);
- // token下发到客户端存入cookie中进行保存
-
- // 再次登录时cookie携带着token到redis中找到对应的value不为空,表示该用户已经登陆过了,如果查询结果为空,则让该用户重新登陆,然后将用户信息保存到redis中。
- // 一般设置一个过期时间,表示的就是多久后用户的登录态就失效了。
先说一下核心参数:
一个任务进来,先判断当前线程池中的核心线程数是否小于corePoolSize
。小于的话会直接创建一个核心线程去提交业务。如果核心线程数达到限制,那么接下来的任务会被放入阻塞队列中排队等待执行。当核心线程数达到限制且阻塞队列已满,开始创建非核心线程来执行阻塞队列中的 业务。当线程数达到了maximumPoolSize
且阻塞队列已满,那么会采用拒绝策略处理后来的业务。
一、限流、削峰部分的设计。
入口大流量限制
例如有10W用户来抢购10件商品,我们只放100个用户进来。
采取发放令牌机制(控制流量),根据商品id和一串uuid
产生一个令牌存入redis
中同时引入了秒杀大闸,目的是流量控制,比如当前活动商品只有100件,我们就发放500个令牌,秒杀前会先发放令牌,令牌发放完则把后来的用户挡在这一层之外,控制了流量。
获取令牌后会对比redis
中用户产生的令牌,对比成功才可以购买商品
- // 设置秒杀大闸
- redistemplate.opsForValue().set("door_count"+promoId, itemModel.getStock()*5)
- // 发放令牌时,先去redis获取当前大闸剩余令牌数
- int dazha = redistemplate.opsForValue().get("door_count"+promoId)
- if (dazha <= 0) {
- // 抛出一个异常
- throw new exception;
- }else {
- String tocken = UUIDUtils.getUUID()+promoId;
- // 用户只有拥有这个token才有资格下单
- redistemplate.opsForValue().set(userToken, token);
- }
高并发流量的限制(泄洪):利用线程池技术,维护一个具有固定线程数的线程池。每次只放固定多用户访问服务,其他用户排队。另外一种实现方式就是J.U.C
包中的信号量(Semaphore)机制。可以有效的限制线程的进入。
二、用户登录的问题(分布式会话)
做完了分布式扩展之后,发现有时候已经登录过了但是系统仍然会提示去登录,后来经过查资料发现是cookie和session的问题。然后通过设置cookie跨域分享以及利用redis
存储token信息得以解决。
redis
设置热点数据永不过期CPU密集型业务:N+1
IO密集型业务:2N+1
基础架构下的tps是200
经过做动静分离、nginx
反向代理并做了分布式扩展、引入redis
中间件后达到了2500 tps。
轮询、权重、IP_hash、最少连接。
首先多台设备登录属于SSO问题,用户登录一端之后另外一端可以通过扫码等形式登录。虽然用户登录了多台设备,但是用户名是一样的。为用户办法的token是相同的。我们为一个用户只会颁发一个token。
设置最大线程数来限制浪涌流量
- ThreadPoolExecutor.AbortPolicy://丢弃任务并抛出RejectedExecutionException异常。
- DiscardPolicy://丢弃任务,但是不抛出异常。
- DiscardOldestPolicy://丢弃队列最前面的任务,然后重新提交被拒绝的任务
- CallerRunsPolicy://由调用线程(提交任务的线程)处理该任务
无效,会从redis中删除,
设置为秒杀商品的个数减去核心线程数最合适。
- jstat -gc vmid count
- jstat -gc 125385000// 表示将12538进程对应的Java进程的GC情况,每5秒打印一次
跟随用户的请求会动态变化,令牌桶机制可以控制每秒生成令牌的个数。
redis中库存减成功后,生成一条消息包含了商品信息、用户信息消息由MQ的生产者生产,经由queue模式发送给消费方,即订单生成的业务模块,在该模块会消费这条消息,根据其中的信息进行订单的生成,以及数据库的修改操作。
TPS:单机2000
QPS:
item表、item_stock表、order表、用户信息表、
将查库存、减库存两个sql
语句作为一个事务进行控制,保证每一个库存只能被一个用户消费。两条语句都执行成功进行事务提交,否则回滚。但这样会导致并发很低。但也没办法。
update table set stock = stock-1 where prom_id = ? and stock >1;
**前端限制:**一次点击之后按钮置灰几秒钟。
**后端限制:**由于秒杀令牌的设置,用户的一个下单请求会先判断用户当前是否已经持有令牌了,因为用户全局只能获取一次令牌,然后存入到Redis缓存中。用户有令牌的话直接返回 “正在抢购中”。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。