架构设计思考
在分层时间设计上,预售中心、订单中心、库存中心、支付中心和管理后台功能独立,职责清晰,快速支持业务。
系统稳定
在架构设计上,接入统一网关让系统安全,限流,对库存中心和订单中心进行数据隔离,加入多级缓存方案,让系统更稳定
秒杀场景高并发如何抗
瞬时流量非常高,对系统的高性能要求很高,三点:热点数据隔离、流量消费漏斗、多级缓存。
数据隔离:对已经完成的订单、预售活动进行历史归档,且采用分库分表,减少系统性能压力,提高吞吐量。
业务隔离:冷热数据离,对热数据进行缓存预热,定时检查缓存与数据库不一致作业
流量消费漏斗
漏斗式的减少请求流量,在业务链路的过程中,进行业务校验、层层过滤。如用户的账号安全、购买资格等是否正常,购买商品的信息状态是否正常、订单状态是否合法、秒杀是否已经结束等。每个层次都要尽可能的过滤掉非法的请求,在最后端处理真正的有效的请求,最终减少请求到数据DB写操作的流量。保证系统处理真正有效的请求。
多级缓存
重复订单问题
限数量、限ip、限pin和限制ip与pin
1、(用户善意行为)用户第一次单击“提交订单”按钮后对按钮进行置灰,禁止再次提交订单,防止由于后端接口没有返回,用户以为没有操作成功会再次单击“提交订单”按钮。
2、黑客直接刷单,绕过前端重提交功能,采用令牌机制,每次用户进入结算页,提单系统会颁发一个令牌ID(全局唯一),当用户点击“提交订单”按钮时,发起的网络请求中带上这个令牌ID,这个时候提单系统会优先进行令牌ID验证,令牌ID存在且令牌ID访问次数等于1才会放行进行处理后续逻辑,否则直接返回。
3、提单系统重试,比如提单系统为了提高系统的可用性,在第一次调用库存系统扣减接口超时后悔重试再次提交扣减请求。提单系统重试,这种情况则需要后端系统(比如库存系统)来保证接口的幂等性,每次调用库存系统时均带上订单号,库存系统会基于订单号增加一个分布式事务锁
int ret=redis.incr(orderId);
redis.expire(orderId,1,TimeUnit.MINUTES);
if(ret!=1){ //添加失败
return "操作失败!原因:重复提交!";
}
if(ret=1){ //添加成功,说明之前没有处理过这个订单号或者1分钟之前处理过了
boolean processFlag=processOrder(orderInfo);
if(!processFlag){
return "操作失败!";
}
return "操作成功!";
}
4、使用数据库唯一键,在锁表中,设定用户订单Id和用户pin作为唯一键。锁定订单时,如果订单号已经存在,会报出数据库异常,不允许某一个商品重复售卖。
库存数据的会滚机制如何做
需要库存会滚的场景:
1、用户未支付, 用户下单后悔了
2、用户支付后取消,用户下单然后支付后后悔了
3、风控取消,风控识别到异常行为,强制取消订单
4、耦合系统故障,比如提交订单时,订单提交系统同时调用积分扣减系统、库存扣减系统、优惠券系统。假如扣减系统、库存扣减系统成功后,调用优惠券扣减失败,需要会滚用户积分与商家库存。
其中场景1、2、3比较类似,都会造成订单取消,订单中心取消后会发送mq出来,各个系统保证自己能够正确消费订单消息MQ就可以了。
对于耦合系统故障,还存在一种极端的情况,如果提单系统准备会滚的时候自身也宕机了,那么库存系统、优惠券系统必须依靠自己完成会滚操作,也就是说要具备自我数据健康检查能力,利用订单号状态为成功的特点,可以通过worker机制,每次抓取40分钟之前的订单,这里40分钟一定要大于容忍用户的支付时间,比如30分钟,调用订单中心查询订单的状态,确保不是自己取消的,否则进行自我数据的会滚。
扣减库存
现实中同一件商品可能会出现多人同时购买的情况,如何做到并发扣减库存呢?
伪代码实现
int ret= updateSQL("update presale_count set count=count-buyNum where count>buyNum; ");
if(ret!=1){
reutrn “扣减成功!”;
}
return "扣减成功!";
如果是促销的秒杀商品,海量的用户秒杀请求,本质上是一个排序,先到先得,但是如此之多的请求,注定了有些人是抢不到的,可以在进入DAO层查询数据库之前增加一个计数器进行控制,比如有50%的流量将直接告诉其抢购失败,伪代码如下:
public class SeckillServiceImpl{
private long count=0;
public String buy(User user ,int productId,int productNum){
count++;
if(count%2=1){
Thread.sleep(1000);
return "抢购失败!";
}
return processBuy(user,productId,productNum);_
}
}
同一用户不允许多次抢购同一件商品,伪代码如下:
public String doBuy(User user,int productId,int productNum){
//用户除了第一次进入为1 ,其他时候均大于1
int tmp=redis.incr(user.getPin()+productId);
if(tmp!=1){
return "抢购失败!";
}
redis.expire(user.getPin()+productId,3600); //1小时候key 自动删除
processBuy(user,productId,productNum);
}
僵尸账号处理
如果同一个用户拥有不同的账号,来抢购同一件商品,一些公司在发展早期是几乎没有限制,注册了很多账号,也就是网络中的“僵尸账号”,数据量庞大,如果使用几万个“僵尸号”去抢购,会提高他们的中奖概率。为了应对这种情况,伪代码如下:
public String ipLimit(User user,Long productId,int productNum){
String redisLimitKey=DateTimeUtil.getDateTimeStr("yyyyMMddHHmm");
String redisLimitCount=redis.incr(redisLimitKey+user.getClientIp());
//threshold为允许每分钟单个ip的最大访问次数
if(minuteIpCount>threshold){
//识别部分潜在风险用户时,让这部分用户强制跳转到验证码页面进行校验
//校验通过后才能继续抢购商品
return sendVerificationCode(user);
}
return processOrder(user,productId,productNum);
}
注意不要用 SimpleDateFormat 因为 SimpleDateFormat线程不安全 可以考虑使用joda-time
备战双十一 618
系统的稳定性都是第一要素:多轮全链路压测、限流、降级、动态扩容、流量调度、减少单点、依赖简化等方式
战前阶段:
1、梳理薄弱点,包括系统架构、系统薄弱点、核心主流程,识别出来后制定应对策略,数据链路、架构瓶颈
2、限流配置,为系统配置安全的、符合业务需求的限流阈值
3、全链路压测预测水位,对系统进行全链路压测,找出系统可以承载的最大QPS
4、应急预案,业务应急预案,稳定性应急预案、该开的权限都开,收集各个域的课能风险点,制作应急处理方案
5、安全保障,安全管控、监控、漏洞排查,主要聚焦在账号权限管控,以最小够用的原则为准,防止权限滥用,安全无小事;
6、战前演练,业务预留演练,稳定性应急预案演练,提高团队响应和处理能力;
7、作战手册,明确具体作战流程和团队间沟通机制,知道出了事该找谁。
精确监控
通过监控,实时发现各个服务是否触发限流值,及时进行Review,调整限流值,保证业务成功率和系统稳定。
对系统基础和业务量指标进行精准监控,比如load,内存、PV、UV,错误量等,避免内存泄露或代码的Bug对系统产生影响,精准监控,提前感知内存泄露等问题。
数据大盘
通过数据大盘,实时汇总数据,展示业务数据,为系统、业务提供更加直观的业务支持,也可以更加有效的进行业务备战。
网友评论