秒杀系统的场景特点:高并发、请求数量远远大于库存数量、下订单减库存。
秒杀架构设计理念:限流、削峰、异步处理、缓存、可拓展。
实现方案:
限流、采用消息队列缓存、利用缓存应对读写请求。
使用技术:
前端:小程序
后端:Spring Cloud、Mybatis
中间件:RabbitMQ、Redis
关键技术:
1、无状态登录(实现分布式session):
- 好处:不依赖于服务端信息(用户信息只存在于user模块)、减小服务端存储压力、服务端可以任意迁移伸缩。
- 流程:登录时对用户信息进行认证->认证通过后将用户信息加密形成token返回客户端->每次请求都携带该token->服务对token进行解密。
- 技术实现JWT+RSA非对称加密:
JWT(Json Web Token),是JSON风格轻量级的授权和身份认证规范。
JWT包含三部分:Header(token类型,最基础是JWT、加密算法),Payload(有效数据)、Signature(RSA256(Base64(Header)+Base64(Payload)+私钥,用于校验信息是否被篡改)。
RSA256:非对称加密。私钥加密时,持有私钥或公钥解密;公钥加密,持有私钥才可解密。
为什么要使用RSA256?如果使用非可逆加密,由于安全性,私钥只会存在鉴权中心,因此所有请求都要访问鉴权中心,导致鉴权中心压力过大。
私钥只存在于鉴权模块中,公钥存在于其他所有模块,所有模块都可以通过公钥解析用户身份信息。
2、Redis预减库存减少数据库访问,内存标记减少Redis访问:
读取策略:首先尝试从缓存读取,读取到数据则直接返回;如果读不到就读数据库,并将数据库写回到缓存,并返回。
更新策略:先更新数据库,然后把缓存对应的数据删除,防止频繁更新,懒加载的思想。如果先删除缓存再更新数据库:如果有A、B线程同时更新,这时C线程在AB更新之间读取,会把A更新的数据写入缓存,导致缓存、数据库不一致;如果不删除缓存,A、B线程更新数据库,B后更新的数据库,却先更新了缓存,也会导致缓存、数据库不一致。
但是如果redis删除失败,也会存在缓存不一致问题,更好的做法是:对相同的商品id做hash,将对于商品的操作路由到某一个内存队列中,以此来保证多线程环境下对同一条记录读写并发的问题,其中可以对多个更新缓存请求合并,并且设置超时时长,防止频繁更新导致大量读请求积压,但一般来说更新频率不会这么高。当读请求并发量很高时可以扩容。但是缺点是串行化,降低系统吞吐量。
3、使用消息队列完成异步下单:
秒杀逻辑:
1)把商品库存加载到Redis;
2)后端受到秒杀请求,Redis预减库存,如果库存不足直接返回失败;
3)库存充足且无重复秒杀,则将秒杀请求入队,返回排队中;
4)后端消费者监听秒杀消息通道,在此判断库存、判断是否重复秒杀,然后执行秒杀事务。
5)秒杀成功则返回商品ID,-1秒杀失败,0表示排队中。
4、安全限制:
1)秒杀接口地址隐藏:秒杀请求发起之前先去获取秒杀地址->生成随机MD5字符串,存入redis,并设置有效期(60s)->前端携带该参数进行秒杀,后端验证该参数;
2)数学公式验证码:ScriptEngineManager eval生成结果存入redis;
3)接口限流防刷:使用用户ID和商品ID拼接key,用key记录用户访问次数,有效期1分钟,并设定访问限制次数。
项目细节问题解决:
1、为防止缓存穿透,对不存在的数据缓存空数据;
2、秒杀开始前进行缓存预热,并使用keepalived建立主从热备;
3、对于缓存中库存已清零,采用内存标记,减少redis服务器的压力;
4、在秒杀订单表中使用商品Id和goodsId建立唯一索引,防止重复下单;
5、在减库存sql中增加stock_count>0判断,防止卖超;
6、设置定时任务轮询超过30分钟未支付的订单,将其关闭,并回复库存。
压力测试:
使用JMeter测试:5000个线程*10次,优化前后QPS从1000多提升到2700多。
改进方法:
nginx横向扩展、分布式缓存、分布式数据库。