UserController("/user")
获取验证码("/getotp")
- 随机生成10000~99999的数字
- 将验证码同对应手机号关联,即使将 < 手机号, 验证码 > 这个KV对存到session中
- 模拟短信发送(仅仅将其打印出来)
- 返回通用对象
注册("/register")
- 根据手机号从session中获取验证码
- 若验证成功,则调用userService的注册方法(注册过程是一个事务)
- 注册内容主要就是合法性判断,数据库的更新等等(使用MD5对密码进行加密)
获取User信息("/get")
- 调用UserService的接口方法,根据userid来获取用户信息
- 返回
登录("/login")
- 登录参数的合法性校验(手机号, 密码)
- 根据登录参数,判断能否登录成功(即判断用户名密码是否正确)
- 如果正确的话,就生成一个登录成功的Token,存储到Redis中(该Token为< UUID, userModel > ),并设置失效时间为1个小时
- 返回TokenId(用UUID生成的Token的唯一标识符)
ItemController("/item")
创建商品("/create")
- 根据传入参数来调用创建商品的Service(往数据库里写对应商品信息,没什么好说的)
促销商品的发布("/publishpromo")
- 调用商品的促销模块来发布商品,除了创建对应的促销商品实体之外,最重要的是将促销商品的库存信息预存到redis中.(提前把火爆的热键存到redis中)
- 同时设定 秒杀大闸 (这个是干嘛的忘了。。看到后面再补充把 todo
商品详情页的浏览("/get")
- 先尝试从本地缓存获取数据(使用的是Guava中的Cache)
- 再尝试从redis从根据商品id获取商品信息
- 如果都没获取到,就去访问数据库(访问数据库的时候,需要先通过布隆过滤器的判断,防止缓存穿透)
商品列表("/list")
- 没啥好说的,也是调用Item的服务…
OrderController("/order")
初始化方法:
- 线程池初始化
- RateLimiter初始化
生成验证码("/generateverifycode")
- todo
生成促销商品下单令牌("/generatetoken")、
之所以需要使用这一步,是为了防止下单接口被刷。 在前端点下下单按钮的时候,会先申请一个促销商品下单的令牌,只有获取这个令牌,才能在之后购买促销的商品
- 尝试从redis中根据tokenID获取用户登录信息(token),获取不到说明没登录
- 验证希望获取的商品令牌的商品是否是正在促销的商品(这里的下单令牌仅对促销商品有用
- 根据用户id,促销商品id, 商品本身id,来生成“促销商品下单令牌”,并返回
下单
本次实战,主要也是对该下单接口进行一个优化.
-
使用RateLimiter对象进行限流(tryAcquire()方法如果返回false,就直接返回对应异常)
-
(Session检查)根据request中的UUID,尝试获取redis中的用户信息(如果获取不到说明没有登录)
-
(合法性检查)校验是否已经获取过“都小商品下单令牌”,并验证是否合法
-
生成一个库存流水(用于消息回查)
-
基于rocketmq,调用消息服务,发送一条事务消息。
- 消息服务(MQProducer)是个单例Bean,在初始化方法中,会对自身进行一个初始化,包括设置NameservAddr,设置listener等等…
- 事务消息的两个回调函数的实现内容:
- 在回调方法中(
executeLocalTransaction
),执行本地事务,这里其实就是将本地事务的执行和消息的发送绑成了一个事务,若本地事务执行成功,则消息才能发送成功,若本地事务执行失败,则消息回滚,依然对消费者不可见。 - 这里的本地事务就是对redis中的库存进行扣减,并生成对应的订单等操作…
- 在回查方法中(
checkLocalTransaction
), 会根据库存流水的状态,来决定当前消息的状态。 即具体逻辑就是,传入的参数里有库存流水ID,根据这个ID去查询对应的下单流水信息,如果status状态是1的话,就说明是正在等待处理,依然是unknown,如果是2的话,则说明已经处理完了,直接返回commit,如果是3的话,说明发生了回滚,就返回回滚状态. - 同时在库存流水中也添加一个字段,即回查该消息的次数,如果超过的一定的次数则判定该消息出现问题,对其进行一个回滚…(例如在消息COMMIT之前出现了宕机,那么该消息就一直处于了UNKNOWN状态…所以,如果处于UNKNOWN状态过久的话,就判断出现了问题。。对其进行回滚)
- 在回调方法中(
- 如果事务消息发送成功,则本地事务一定执行成功,所以也就表明下单成功。。。(这里只解决了保证消息投递成功,但没解决消费端一定能消费成功)
另一端:
- 对于消费端而言,会消费生产者投递的库存扣减的消息。。。
如何设计一个秒杀系统
问题一:瞬时高并发
时间极短, 瞬间用户量大, 出现缓存雪崩,缓存击穿,缓存穿透等情况,打挂DB可能会造成系统的联级失效。
1.1 服务单一职责——防止联级失效
单独提供一个秒杀服务模块,数据库也采用分库的思想,单独提供一个秒杀库。 这样做的好处就是即使秒杀库挂了,服务挂了,也不会影响到其他业务。
1.2 Redis集群——解决单台redis性能瓶颈
单机的Redis顶不住高并发的问题可以通过搭建Redis集群来解决,写master,读slave,然后开启哨兵模式,开启持久化保证高可用。
1.3 Ngnix反向代理——解决单台服务器性能瓶颈
一台ngix服务器负责反向代理,多台web服务器提供服务。
1.4 缓存雪崩、缓存击穿、缓存穿透问题也要考虑
- 缓存雪崩
- 问题:当缓存层出现问题没法工作时,流量会打到后台数据库上,会出现问题。
- 解决方法:
- 保证redis高可用
- 进行限流(可以使用线程池,信号量,Guava的RateLimiter等来进行限流)
- 缓存穿透
- 问题:访问缓存和数据库中都没有的数据,这种请求会穿过缓存层打到数据库上,若有大量的这种请求,那么缓存层就形同虚设了…
- 解决方法:
- 缓存空数据(不推荐,因为组合出数据库里不存在的数据的方式很多,这么做不仅没法解决问题甚至还会让redis挂掉…)
- 布隆过滤器:布隆过滤器能够保证无法通过的数据一定不存在,所以,可以事先把数据库里的主键给预存到布隆过滤器里,只有通过了布隆过滤器的那些访问请求,才会去redis,数据库中去查找.
- 缓存击穿
- 问题: 一个超级热点数据如果因为超时失效或其他原因而从redis中被删除,那么短期大量的流量就会打到数据库上.
- 解决办法:
- 对于热点数据不设置失效时间
- 提前将热点数据预存到redis中
- 使用第三方缓存或本地缓存(例如Guava的Cache),如果是热点数据的话,频繁被访问的情况话就不会被置换出去
- 限流(线程池、信号量、RateLimiter。。。)熔断(to study)
1.5 消息队列保证Redis和Mysql的最终一致性
- 使用rocketmq的事务型消息 ( rocketmq的事务型消息保证了若成功发送了commit消息那么本地事务一定成功执行了(ps:只有commit的消息才能被消费者看到,而prepare的消息无法被看到) ):
- 在本地事务中执行对Redis等其他不是太消时间的,比较关键的操作,对mysql的同步交给消费者去做.
问题二:超卖问题
就比如现在库存只剩下1个了,我们高并发嘛,4个服务器一起查询了发现都是还有1个,那大家都觉得是自己抢到了,就都去扣库存,那结果就变成了-3,是的只有一个是真的抢到了,别的都是超卖的。
- 解决方法:
- 增加全局售罄标志,如果发现秒杀商品已经售罄,那就直接返回了.
- 使用Lua脚本实现Redis的CAS功能。Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。写一个Lua脚本把判断库存和扣减库存的操作都写在一个脚本丢给Redis去做。(to study)
- 使用分布式锁来对redis的库存个数进行上锁然后再进行判断,扣减.
问题三:恶意刷单
- 让前端控制,秒杀商品的下单按钮在秒杀前置灰。
- 使用秒杀令牌,在页面端点下单按钮的时候,会先向服务器申请一个秒杀令牌,然后才是真正的下单,在下单时会去检查这个秒杀令牌是否存在,是否合法。
- 服务端限制同一个用户的最大下单个数?
- 封IP(从nginx这边来控制)
- 使用验证码
考虑的点:
- 如果秒杀的流量单机无法抗住的话,就用集群+分布式来扩展…(1.3 1.2)
- 单独提供一个秒杀服务模块,数据库也采用分库的思想,单独提供一个秒杀库。 这样做的好处就是即使秒杀库挂了,服务挂了,也不会影响到其他业务,对于数据表也进行分表(库存单独分离出来)。
- 充分利用缓存(多级缓存的思路,本地缓存Guava的Cache,第三方缓存,redis缓存,缓存带来的问题(穿透,雪崩,击穿))
- 消息队列保证redis和mysql的最终一致性
- Redis中的共享数据(比如在redis中的库存),在多线程环境下使用分布式锁,用redis的setnx + px 来做
- 考虑到恶意下单,看上面的问题3
- 考虑到限流量,可以使用Guava的RateLimiter,信号量,线程池来做…
问题:
Q: 库存预热:
A: 这个一般给运营人员去做吧。。手动将秒杀商品的库存加载到redis里面,在规模比较大的时候的话,可以专门写一个这种管理平台系统吧…
Q: 扣减库存怎么保证线程安全:
A: 项目里是通过分布式锁来去做的(todo 实现一下分布式锁), 就是向上提供一个查询接口,如果获取不到锁就自旋…
Q: 缓存击穿
A: redis预热、永不失效、本地缓存或第三方缓存、限流策略…
Q: 客户的恶意下单怎么防范
A: 秒杀令牌机制。 其他还有: 验证码,封ip(nginx有个设置项,单个ip访问的频率和次数多了之后有个拉黑操作…服务端写逻辑封用户?),(接口隐藏 )秒杀前将下单接口置灰,使获取不到下单接口… 或者也可以限制同一个用户的最大下单数?