秒杀系统通常需要考虑:高性能、高可用、一致性、限流
高性能
一个秒杀活动,不可避免的涉及到高并发。在高并发场景下,通常将数据放入redis缓存(设置过期时间),然后再放入JVM内存(减少网络开销)。对于一些静态资源比如图片之类的,通过CDN (内容分发网络)处理,将图片之类的分发到不同地方以实现就近访问。
一般来说,如果是商品秒杀或者演唱会门票秒杀这类,我们明知特定商品和接口会迎来高并发,就需要提前进行缓存预热,将商品或者门票提前写入缓存中。
这类数据称为热点数据
热点数据分为静态和动态。
静态就是上述的门票或者商品。
动态可以理解为某个商品突然爆火,我们不可预知的,需要上手段才能检测出来的。
动态检测有哪些手段:
中间件:例如京东的hotkey。
或者通过redis快速定位热key:比如
1 通过代理,所有的redis请求都通过代理,改动 Proxy 代码收集信息,可以解决多语言问题,但是开发难度大
2 通过reids定时扫描,在redis4.0之后,可以通过 redis-cli --hotkeys 获取当前的热key。实际上是通过scan+object freq完成。但由于要扫描所有key,耗时大,效果不好
3 reids节点抓包,在可能产生热key 的节点上进行抓包并解析计算。不过会造成给节点增加负载
高可用
一般而言都是集群。例如redis集群,使用Sentinel哨兵+一主多从结构。
一致性
一般就是两个一致性,库存数量和扣款金额
库存一般采用下单就扣库存,例如抢票的时候,会有10分钟支付时间,虽然还没支付,但是库存已经扣掉,只有超时没付钱或者关闭付费才释放库存。
通过redis+LUA脚本实现。通常数据一致性是通过更新数据库,再删除缓存来保证一致性。
在这里确定redis扣减库存后,可以通过先对redis的库存进行更新,然后通过MQ保证消息被消费了就行。这样确保数据库最终和缓存是一致就行。
对于金额就要慎重:
金额需要强一致性。并发量高的时候使用悲观锁,低的话使用乐观锁(版本号)。数据库还要确保唯一索引。
悲观锁一般通过redis或zookeeper实现分布式锁。也可以通过mysql的排他锁(innodb,事务)
redis一般是通过redisson来实现的,在redis深入中有所介绍
zookeeper的分布式锁基于 临时顺序节点 和 Watcher(事件监听器) 实现的。
获得锁
- 首先我们要有一个持久节点
/locks
,客户端获取锁就是在locks
下创建临时顺序节点。 - 假设客户端 1 创建了
/locks/lock1
节点,创建成功之后,会判断lock1
是否是/locks
下最小的子节点。 - 如果
lock1
是最小的子节点,则获取锁成功。否则,获取锁失败。 - 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如
/locks/lock0
上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。
释放锁:
- 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
- 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
- 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
幂等
幂等问题是确保消息只被消费一次。
实际项目中通过分布式锁来保证:redis、zookeeper。(在短链接项目中有用到)
或者通过token机制
具体流程步骤:
1. 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID 返回给客户端。
2. 客户端第二次调用业务请求的时候必须携带这个 token。
3. 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token,然后再执行业务。
4. 如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端
限流
限流一般是对接口限流:redis+LUA脚本 和 Sentinel(提供了流量控制、熔断降级等) 两个方法都可以实现。
有的时候也会通过限制用户和IP,比如限制他每秒访问次数,防止脚本抢单
削峰:通过MQ实现,当用户发送秒杀请求后,放入到MQ中,慢慢消费。(如果接口限流了其实不需要削峰)
降级:保证核心业务,如果服务器有压力,且某些页面有降级策略,那么那些页面就会释放服务器资源保证核心业务的进行
熔断:例如电闸的保险丝,如果电流过大,就会使得保险丝断裂,保护整个电箱不受破坏。在分布式系统中,某些服务依赖下游服务,如果下游有问题,例如秒杀在服务A上,服务A上还有其他功能,比如管理某商品之类,此时管理某商品的请求访问时间就会非常长,长时间的等待增加了链路的请求时间,如果后续不断有管理某商品的请求就会导致服务器COU拉满之类的问题,所以就会触发熔断,直接不让大家访问管理某商品的这个服务。