/redis-lua

使用Redis+lua实现秒杀业务的DEMO

Primary LanguagePHPMIT LicenseMIT

redis+lua优惠券秒杀demo

说明

本质是把库存放到redis里做扣减,解决数据库更新库存的行锁瓶颈。

库存流水需要异步插入到数据库中,作为最终的业务凭证。

原理

  • 前端:采用PHP-FPM实现,但是建议生产环境使用GO/JAVA等支持redis长连接的开发语言/框架。
  • 后端:利用Redis维护优惠券库存,利用Redis Lua保障库存扣减幂等性,同时投递库存流水日志到Redis队列。
  • 任务:消费Redis中的库存流水日志,批量插入到数据库中,落地为业务凭证。(我没实现)

概念

  • 优惠券:用户看到的优惠券实体,内部包含多个批次。
  • 批次:一个优惠券由多个批次组成,一个批次内有多个券码coupon。

业务流程

1)创建优惠券
2)创建批次,上传对应的若干券码(或者随机生成)
3)将批次生效到线上,用户可以秒杀

业务取舍

业务规则 - 先验与回滚

兑换优惠券可能需要消耗用户积分,前端必须实时做一次积分查询,积分充足则立即执行Redis扣减。

在任务处理时,对用户积分做真实扣减,若此时积分不足则可以将券码归还到redis中,并站内信通知用户。

因为最终业务凭证是以数据库为准的,而任务本身是一个准实时异步过程,所以业务前端应该给予用户合理的提示,例如"稍后到卡包中查看优惠券"。

业务规则 - 前置与有损

如果业务有类似限制IP领取几次等比较复杂的规则,优先考虑在redis lua之前做前置判定。

因为lua脚本中逻辑越多,redis的TPS越低,所以尽量保证lua只做库存核心扣减。

前置规则校验的缺点,就是可能因为前置规则更新了某些计数,而后续的redis lua扣减异常,导致用户再也无法抢该券码。

但是出于折衷,暂时没有更好的办法,可以根据业务需要做让步和取舍。

性能

云主机Redis:云存在超卖问题,性能也不是很稳定,库存扣减性能在2w-4w/s。
物理机Redis:  库存扣减<=9w/s,随lua逻辑的复杂度而稍微降低,但整体远优于云Redis。

压测均采用redis长连接,采用PHP-FPM无法达到预期,需要换一个语言来实现库存前端服务。

任务入库仅做流水的select/insert操作,可以基于批量插入优化实现高吞吐。

如果业务量需要单个商品/优惠券>=9w/s的秒杀量,可以考虑限流,否则绰绰有余。

限流

因为单个优惠券的库存是单台redis执行lua来承载的,而redis是单线程程序,所以lua的TPS有限。

在单个优惠券后端处理能力已知有限的情况下,有2个解决方案:

1)限流:也就是通过前端挡住大多数流量,只透传可控的流量到redis,给予用户友好的文案即可。
2)扩展:把优惠券的库存拆成N份,存储到多个redis实例,用户按uid打散到不同的redis实例进行扣减。

但是从公司量级来说,除非做到小米和京东的秒杀量级,否则上述方案大概一辈子也用不到。

存储量

demo里直接把优惠券coupon放在redis里,为了节约存储空间可以考虑只放coupon的数据库id。

对于商品类秒杀,库存可能只是一个剩余数量,异步任务需要走商品库存服务完成真实扣减。

lua注意事项

1,不能使用带有随机性质的函数(redis会直接报错),比如time(),spop()等,否则主从同步时在从库上重放脚本,会导致主从不一致。
2,在cluster/codis下,可以使用redis的hash tags机制,实现多个key路由到同一个slot的能力,确保lua可以访问到相关数据。
3,不要使用eval,每次上传脚本到redis性能很差;应该使用evalsha,提前计算好lua脚本的sha1值,当evalsha报错时再触发一次script load上传脚本,也就是懒惰上传。

异常处理

因为最终是数据库作为业务凭证,所以当redis主从切换等数据丢失场景出现时,至多出现少发的情况,绝对不会出现超发,因为在数据库层面可以基于coupon做去重。