Redis 的高并发实战:抢购系统 | 学习笔记

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 快速学习 Redis 的高并发实战:抢购系统

开发者学堂课程【Redis 入门及实战:Redis 的高并发实战:抢购系统】学习笔记,与课程紧密联系,让用户快速学习知识。

课程地址:https://developer.aliyun.com/learning/course/777/detail/13653


Redis 的高并发实战:抢购系统

内容介绍:

一、 IO 模型和问题

二、 资源竞争与分布式锁

三、 Redis 抢购系统实例

 

一、IO 模型和问题

(一) Run-to-Completion in a solo thread(在一个单独线程中运行到完成)

Redis 的社区版 IO 模型比较简单,通常是有一个单独线程去实现所有的命令的解析和处理的。那么有一个比较严重的问题,是当进行一个慢查询的命令时,其的命令都要去排队,那么后面的命令都会被阻塞慢。那么会让使用 ping 命令也被延迟,如果引擎被卡住,则 ping 失败。若一个慢查询超过 30 秒钟之后,就会拖慢 ping 命令,无法去判断服务,此时是不是可用的。

image.png

Epoll-wait 向 TCP recvO、Process、TCP sendO 这三部分传输的过程:

1.用户所有的来自不同 client 的请求,实际上在每个 event 到来后,交由后端单线程执行。等每个 event 处理完成后,才处理下一个

2.单线程 run-to-completion 就是没有 dispatcher,没有后端的 multi-worker

Process 双向传导的过程:

如果慢查询诸如 keys、 lrange、 hgetall 等拖慢了一次查询,那么后面的请求就会被拖慢

使用 Sentinel 判活的缺陷∶

ping 命令判活: ping 命令同样收到慢查询影响,如果引擎被卡住,则 ping 失败

duplex Failure : sentinel 由于慢查询切备(备变主)再遇到慢查询则无法继续工作

image.png

(二) Make it a cluster (把变成一个集群)

当把单分片,用多个分片组成一个 cluster 的时候这样的话是一样的问题。

1. 同样的,集群版解决不了单个 DB 被卡住的问题

2.查询空洞:如果用户调用了跨分片的命令,如 mget,访问到出问题的分片,仍会卡住

(三) "Could not get a resource from the pool"(“无法从池中获取资源”)

客户端一般使用 Redis 的客户端,该客户端会配一个连接池,业务线程去访问的时候,每一个查询会去里面取一个远程连接,然后进行访问。

之所以使用连接池,是因为 Redis 协议不支持连接收敛

Message 没有 ID,所以 Request 和 Response 关联不起来

非常类似 HTTP 1.× 消息

sLow

当 Engine 层出现慢查询,就会让请求返回得慢

很容易让用户把连接池用光

当应用机器特别多的情况,按每个 client 连接池 50 个 max_conn 来算,很容易打到10 K 链接的限制,导致回调速度慢

Redis client

1.每次查询,都要先从连接池拿出一根连接来,当返回后,再放回连接池

2. 如果用户返回的及时,那么连接池一直保有的连接数并不高

但是一旦返回不了,又有新的请求,就只能再 checkout 一根连接

当连接池被 checkout 完,就会爆没有连接的异常:"Could not get a resource from the pool"(“无法从池中获取资源”)

(四) Redis 2.x/4.x/5.x 版本的线程模型

Redis 2.x/4.x/5.x 版本等,几个比较知名的的版本的模型是没有发生变化过的,就是说所有的业务线程以及所有的读写、处理都在一个主要的 IO 里面去运行。后台有几个 B I O 线程,任务主要是关闭文件以及刷文件等,4.x 之后增加了LAZY_FREE,就是说有些大 K,可以一步去释放掉。

在 Redis 2.x 上会遇到一些比如淘汰或者过期删一些比较大的 K 的时候服务会卡顿,所以建议不用 4.x 以下的服务端。

(五) Redis 5.x 版本的火焰图

简单的性能分析,一个 IO 线程用一些简单命令,比如 O E 的命令去运行的时候,发现所有的消耗基本上在该网络的 IO 上。

图前两部分是面临处理,然后中间的这一块儿小的是读,最右侧是写占 1.16%。这个占比能够知道性能,基本上都被耗在了网络 I O 上。

image.png

image.png

(六) Redis 6.x 版本的线程模型

Redis 6.x 版本是改进的模型,这个模型可以在主线程读完,可读事件出发之后,可以把这个读任务去委托给 I O 现场来处理。等全读完之后,返回这个结果,再进行处理,也可以分发给 LAZY_FREE 来写,这样可以提升性能,但是引擎还是只有一个。

有一些 O E 的命令,比如简单的读写,效果是非常高的,但如果命令本身很杂,因为 D B 的还是只有一个线程,效果是比较差的。这样的处理还有个问题是,比如说把这个读任务委托之后,要等返回,写也要等返回,所以主线程实际上有很大的时间是在等的,这段时间,也是没有办法提供服务的。所以说这个模型还是有一些能够提升的空间的。

(七) 阿里云 Redis 企业版( Tair 增强性能)的线程模型

image.png

云上的企业版的主线程,只负责命令处理,所有的读写处理由 L O 线程全权负责,连接不再是永远都属于主线程,当一个客户端,连进来之后,这个客户端就直接交给了其的 I O 线程。从此这个客户端可读可写的所有的事件,不需要去理会。之后有命令的话会转发给主线程来处理,处理之后通过通知方式,转给其的线程,进行写。这样可以最大程度的把主线程的等待时间给省掉,性能会更进一步的提升。当然缺点是一样的,因为毕竟还是只有一个线程在处理。所以对于 OE 的命令提升效果非常的理想,但对于这种本身比较耗 cpu 的命令的效果并不理想。

(八) 性能对比测试

左边的灰色的是 Redis.5X,右边是这个性能增强版,然后 Redis.6X 大概在这两个之间的一个范围之内,当然这个命令是 OE 的命令,本身不是耗 cpu 的,瓶颈是在 I O 上的,所以效果非常理想。

最坏情况下,假设命令本身是特别耗 cpu 的,那么这两个值会无限的逼近,直到两个值会齐平。

 

二、资源竞争与分布式锁

(一) CAS/CAD 是对 Redis String 的扩展

分布式锁实现的问题

续约(使用 CAS )

详细档: https://help.aliyun.com/document_detail/146758.html

CAS/CAD 以及后续提到的 TairString 以 module 形式开源:https://github.com/alibaba/TairString

设置

SET resource_1 random_value NX EX5

删除

CAD resource_1 my_random_value

续约(一般有一个 Watchdog 机制去续约)

CAS resource_1 my_random_value my_random_value Ex 10

分布式锁的实现,在 Redis 上的实现是比较多的,网上有很多的教程来帮助去实现一个分布式锁。比如,Redis 本身的字符串的写命令,有一个参数叫 N X,就是说这个字符串儿不存在的时候,可以写那这样就是一个天然的一个加锁场景,因为本身就支持这样的一个特性,所以去加锁非常容易。然后给这个资源一个随机值,然后去写带一个 N X 就可以了,要带一个过期的原因,比如业务机器加上锁之后,如果因为某个原因被下线掉了,如果这个锁没有释放掉,那么使得这个锁永远无法被解除,所以需要一个过期时间来保证业务机器,有故障之后这个锁一样是会被释放掉的。5 秒不是一个固定的过期时间,过期时间要看机器。

删除的时候比较麻烦,比如这个机器加上锁之后突然遇到了一些情况,比如卡顿或者某种原因失联了。失联之后,因为这个锁过了五秒,已经失效了,然后其的机器加上了这个锁,若之前那个机器又可用了,但是处理完之后,之前那个机器掉了一个 dell,K 删掉了。这就使得删掉了本来并不属于这个机器的锁。所以这个删除需要一个判断,就是说当这个 value 等于之前写的 value 的时候,才可以删掉。

续约是 CAS。因为去加一个锁的时候给了一个过期时间,比如说五秒,那如果说业务会有一个机制去续约,比如说发现这个事务已经过了三秒了,还没执行完,那么续约可以把这个时间延长。这个续约跟删除是一样的道理,不能直接去删需要符合条件的时候,才能续。

这个分布式组并不是特别可靠的,比如说之前加上锁之后失联了,锁过期的被其机器持有了,但是突然又可用,这个时候代码上是不会去判断的,以至于包括其的分失所,并不是 100% 可靠的。

#删除

if redis.call("get", KEYS[1]) == ARGV[1] then

return redis.cal1 (""del" , KEYS[1])

else

return e

end

#续租

if redis.call("get", KEYS[1])== ARGV[1] then

return redis.call ("expire" , KEYS[1],ARGV[2])

else

return e

end

脚本中每次调用会改变的值一定要通过参数传递,因为只要脚本不相同,Redis 就会缓存这个脚本,截止目前社区 6.2 版本仍然没有限制这个缓存大小的配置,也没有逐出策略,执行 script flush 命令清理缓存时也是同步操作,—定要避免脚本缓存过大。

使用方式也是先执行 script load 命令加载 Lua 到 Redis 中,后续使用 evalsha 命令携带参数调用脚本,—来减少网络带宽,二来避免每次载入不同的脚本。需要注意的是 evalsha 可能返回脚本不存在,需要处理这个错误,重新 script load 解决。

其实由于 Redis 本身的数据一致性保证以及宕机恢复能力上看,分布式锁并不是特别可靠的

Redis 作者提出来 Redlock 这个算法,但是争议也颇多︰参考资料 1 参考资料 2 参考资料 3

如果对可靠性要求更高的话,可以考虑 Zookeeper 等其方案(可靠性++,性能-- )

或者,使用消息队列串行化这个需要互斥的操作,当然这个要根据业务系统去设计

(二) Redis LUA

不是特别建议使用 Redis LUA,Redis LUA 的执行,有很多的转换,如解析、翻译,然后再去执行整个过程。使用 Redis LUA 等于是在 C 调用 Redis LUA,然后在Redis LUA 里面去调用 C。这个返回值会有两次的转换,先从 Redis 的这个协议返回值转成 Redis LUA 对象,再由 Redis LUA 对象转成 C 的,这个数据再返回,两次转换。有很多这种 Redis LUA 的解析和处理,包括的这个内存占用,所以会比一般的命令执行要慢。

image.png

如果要使用 Redis LUA,最好只写一些比较简单的 excel 判断,尽量避免循环,尽量避免一些比较重的操作,尽量避免一些大的这种数据的访问和获取。因为引擎只有一个线程,当这个 cpu 被耗在 Redis LUA 上的时候,就只有更少的 cpu 来处理业务的命令,所以要慎用 Redis LUA。

The LUA Iceberg inside Redis

1. 脚本的 compile-load-run-unload 非常耗费 CPU

2. 整个 Lua 相当于把复杂事务推送到 Redis 中执行,如果稍有不慎内存会爆,引擎算力耗光后挂住 Redis

Script + EVALSHA

1. 可以先把脚本在 Redis 中预编译和加载(不会 unload 和 clean ) ,使用 EVALSHA执行

2. 会比纯 EVAL省 CPU,但是 Redis 重启/切换/变配 code cache 会失效,需要 reload

3.仍是缺陷方案。建议使用复杂数据结构,或者 module 来取代 Lua

对于 JIT 技术在存储引擎中而言,“EVAL is evil”,尽量避免使用 Lua 耗费内存和计算资源(省事不省心)

某些 SDK (如 Redisson )很多高级实现都内置使用 Lua,开发者可能莫名走入 CPU运算风暴中,须谨慎

 

三、Redis 抢购系统实例

(一) 抢购/秒杀场景的特点

秒杀活动对稀缺或者特价的商品进行定时定量售卖,吸引成大量的消费者进行抢购,但又只有少部分消费者可以下单成功。因此,秒杀活动将在较短时间内产生比平时大数十倍,上百倍的页面访问流量和下单请求流量。

秒杀活动可以分为 3 个阶段︰

秒杀前:用户不断刷新商品详情页,页面请求达到瞬时峰值

秒杀开始:用户点击秒杀按钮,下单请求达到瞬时峰值

秒杀后:少部分成功下单的用户不断刷新订单或者退单,大部分用户继续刷新商品详情页等待机会

(二) 抢购/秒杀场景的一般方法

抢购/秒杀其实主要解决的就是热点数据高并发读写的问题

1. 抢购/秒杀的过程就是一个不断对请求“剪枝”的过程︰

尽可能减少用户到应用服务端的读写请求(客户端拦截一部分)

应用到达服务端的请求要减少对后端存储系统的访问(服务端 LocalCache 拦截一部分)

需要请求存储系统的请求尽可能减少对数据库的访问(使用Redis拦截绝大多数)

最终的请求到达数据库(也可以消息队列再排个队兜底,万一后端存储系统无响应,应用服务端要有兜底方案)

2. 基本原则:

数据少(静态化、CDN、前端资源合并,页面动静分离,LocalCache )

路径短(前端到末端的路径尽可能短、尽量减少对不同系统的依赖,支持限流降级)

禁单点(应用服务无状态化水平扩展、存储服务避免热点)

3. 扣减库存的时机:

下单减库存(避免恶意下单不付款、保证大并发请求时库存数据不能为负数)

付款减库存(下单成功付不了款影响体验)

预扣库存超时释放(可以结合 Quartz 等框架做,还要做好安全和反作弊)

4. Redis 的一般实现方案

String 结构

直接使用 incr/decr/incrby/decrby,注意 Redis 目前不支持上下界的限制

如果要避免负数或者有关联关系的库存 sku 扣减只能使用 Lua

List 结构

每个商品是一个 List,每个 Node 是一个库存单位

扣减库存使用 lpop/rpop 命令,直到返回 nil ( key not exist ).

Set/Hash 结构

一般用来去重,限制用户只能购买指定个数 ( hincrby 计数,hget 判断已购买数量)

注意要把用户 UID 映射到多个 key 来读写,一定不能都放到某一个 key 里(热点)

业务场景允许的情况下,热点商品可以使用多个 key : key_1 , key_2 , key_3 ...

随机选择

用户 UID 做映射(不同的用户等级也可以设置不同的库存量)

(三) TairString: 支持高并发 CAS 的 String

1. 携带 Version 的 String

保证并发更新的原子性

通过 Version 来实现更新,乐观锁

不能与 Redis 的普通 String 混用

应用:先来去 exSet get 一个值会返回的 Version,然后基于这个 Version 做一些操作,当更新的时候,可以带上之前那个 Version ,如果一致,那么更新,否则重新去读和改,再更新实现一个 exCAS 操作在服务端就是这是一个乐观锁。

对于这个场景,可以进一步优化提供一个叫 exCAS 的操作,这个 exCAS 跟 exSet是一样的,但遇到了 Version 冲突之后,不光是返回一个 Version 不一致的错误,并且会顺带的返回新的 Redis 跟新的 Version。

先去 exSet get 之后,用 exCAS 进行操作,如果失败了再改 exCAS 可以降低对服务端的访问量。

更多的语义

exIncr/exIncrBy:抢购/秒(有上下界)

exSet -> exCAS减少网络交互

详细文档:https://help.aliyun.com/document detail/147094.html

以 Module 形式开源: https://github.com/alibaba/TairString

(四) String 和 exString 原子计数的对比

EXINCRBY

语法及复杂度;

ExiNCReY | ExmNCRBY nkey crumo [EX time] [iPx time] [EXAT time] [EXAT time] [PXAT time] [0NK 1 X0) IVERwersiom | ABS versioni]l [MtIN minvall][MX maomval [NONEGATNME][WITHVERSIDN]

时间复杂度: 0(1)

命令插述:

对 Key 做自增自减操作。num 的范围为 long.

参数播述:

key: 定位 TairString 的键

num: TairString 自增的做值。必须为自然数

EX: 秒级相对过期时间

EXAT: 秒级绝对过期时网

P火: 毫秒级相对过期时间

PXAT: 毫秒级绝对过期时同

NX: 当数据不存在时写入

XX: 当数据存在时写入

VER: 版本号,如果数据存在,和已经存在的数据的版本号做比较。如果相等,写入并版本号加 1,如果不相等,返回出错;如果数据不存在,忽略传入的版本号。写入成动之后,数据版本号变为 1

ABS: 绝对版本号。不论数据是否存在,覆盖为指定的版本号

MIN: TairString 值的最小值

MAX: TairString 值的最大值

NONEGATIVE: 设置后,若 inerby 的结果小于 0 则将 value 置为 0

WITHVERSION: 额外返回一个 version

返回值:

返回类整: Long

成功:引擎的 value 值

String 方式 INCRBY,没有上下界;exString 方式是 EXINCRBY,提供了各种各样的参数跟上下界,比如直接指定最小是 0,当等于 0 时就不能再减了。另外还支持过期,比如某个商品只能在某个时间段抢购,过了这个时间点之后失效。业务系统也会做一些限制,缓存可以做限制,过了时间点把这个缓存清理掉。如果库存数量有限,比如如果没人购买,商品过 10 秒钟消掉;如果有人一直在买,这个缓存一直续期,可以在 EXINCRBY 里面带一个参数,每调用一次 INCRBY 或者 API 就会给它续期,提升命中率。

计数器过期时间可以做什么?

1某件商品指定在某个时间段抢购,需要在某个时间后库存失效

2. 缓存的库存如果有限,没人购买的商品就过期删除,有人购买过就自动再续期一段时间(提升缓存命中率)

#String

if tonumber(redis.cal1 ("get"", KEYS[1]))> e then

return redis.call("decrby-, KEYS[1],KEYS[2])

else

return redis.error_reply ( overflow"")

end

a TairString

exincrby k -1 min e

注意 String 和 TairString 类型不同,不能混用 API

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
17天前
|
存储 缓存 NoSQL
深入解析Redis:一种快速、高效的键值存储系统
**Redis** 是一款高性能的键值存储系统,以其内存数据、高效数据结构、持久化机制和丰富的功能在现代应用中占有一席之地。支持字符串、哈希、列表、集合和有序集合等多种数据结构,适用于缓存、计数、分布式锁和消息队列等场景。安装Redis涉及下载、编译和配置`redis.conf`。基本操作包括键值对的设置与获取,以及哈希、列表、集合和有序集合的操作。高级特性涵盖发布/订阅、事务处理和Lua脚本。优化策略包括选择合适数据结构、配置缓存和使用Pipeline。注意安全、监控和备份策略,以确保系统稳定和数据安全。
220 1
|
1月前
|
NoSQL Linux Redis
Linux系统中安装redis+redis后台启动+常见相关配置
Linux系统中安装redis+redis后台启动+常见相关配置
|
23小时前
|
存储 缓存 NoSQL
Redis入门到通关之Redis缓存数据实战
Redis入门到通关之Redis缓存数据实战
|
8天前
|
人工智能 前端开发 Java
Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
智慧导诊解决盲目就诊问题,减轻分诊工作压力。降低挂错号比例,优化就诊流程,有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位,了解对应病症信息和推荐就医科室。
147 10
|
29天前
|
NoSQL Redis
Netty实战:模拟Redis的客户端
Netty实战:模拟Redis的客户端
14 0
|
29天前
|
监控 NoSQL Java
记一次线上商城系统高并发的优化
记一次线上商城系统高并发的优化
11 0
|
1月前
|
存储 NoSQL Redis
KubeSphere 核心实战之二【在kubesphere平台上部署redis】(实操篇 2/4)
KubeSphere 核心实战之二【在kubesphere平台上部署redis】(实操篇 2/4)
22 0
|
1月前
|
NoSQL Java 数据库
优惠券秒杀案例 - CAS、Redis+Lua脚本解决高并发并行
优惠券秒杀案例 - CAS、Redis+Lua脚本解决高并发并行
|
1月前
|
消息中间件 存储 NoSQL
【Redis项目实战】使用Springcloud整合Redis分布式锁+RabbitMQ技术实现高并发预约管理处理系统
【Redis项目实战】使用Springcloud整合Redis分布式锁+RabbitMQ技术实现高并发预约管理处理系统
|
4月前
|
Java
在高并发环境下,再次认识java 锁
在高并发环境下,再次认识java 锁
36 0

热门文章

最新文章