redis核心技术与实战

redis

Posted by Jesse on February 1, 2022

redis核心技术与实战

开篇

02 数据结构

哈希桶中的 entry 元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到。

当你往 Redis 中写入大量数据后,就可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在的风险点,那就是哈希表的冲突问题和 rehash 可能带来的操作阻塞。

rehash

  • 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  • 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
  • 释放哈希表 1 的空间。

Redis 采用了渐进式 rehash

把一次性大量拷贝的开销,分摊到了多次处理请求的过程中

压缩表

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

跳表

增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位

跳表的查找复杂度就是 O(logN)

不同操作的复杂度

  • 单元素操作是基础;
  • 范围操作非常耗时;统计操作通常高效;
  • 例外情况只有几个。

03 高性能IO模型:为什么单线程Redis能那么快?

Redis 为什么用单线程?

多线程的开销

上下文切换 争抢共享资源

单线程 Redis 为什么那么快?

内存上完成

高效的数据结构,例如哈希表和跳表

Redis 采用了多路复用机制

基本 IO 模型与阻塞点

基于多路复用的高性能 I/O 模型

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。

这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

04 AOF日志:宕机了,Redis如何避免数据丢失?

写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中

三种写回策略

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

AOF日志文件太大了怎么办?

AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件

旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。

AOF 重写会阻塞吗?

和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。

“一个拷贝,两处日志”。

05 内存快照:宕机后,Redis如何实现快速恢复?

RDB 全量快照

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

写时复制技术

主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本(键值对 C’)。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。

如果频繁地执行全量快照

频繁将全量数据写入磁盘,会给磁盘带来很大压力

子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程

内存快照和AOF混合使用

06 数据同步:主从库如何实现数据一致?

  • 读操作:主库、从库都可以接收;
  • 写操作:首先到主库执行,然后,主库将写操作同步给从库。

Redis主从库和读写分离

主从库间如何进行第一次同步?

主从级联模式分担全量复制时的主库压力

主从库间网络断了怎么办?

07 哨兵机制:主库挂了,如何不间断服务?

哨兵机制的基本流程

哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。

主观下线和客观下线

哨兵集群

如何选定新主库?

从库的当前在线状态,还要判断它之前的网络连接状态

第一轮 优先级最高的从库得分高

第二轮 和旧主库同步程度最接近的从库得分高

第三轮 ID 号小的从库得分高。

08 哨兵集群:哨兵挂了,主从库还能切换吗?

基于 pub/sub 机制的哨兵集群组成

主库上有一个名为“sentinel:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的。

哨兵又通过 INFO 命令,获得了从库连接信息,也能和从库建立连接,并进行监控了。

客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。

由哪个哨兵执行主从切换?

09 切片集群:数据增多了,是该加内存还是加实例?

Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。

首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽

每个实例上的槽个数为 16384/N 个

Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散

  • 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
  • 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。

实例之间还可以通过相互传递消息,获得最新的哈希槽分配信息

Redis Cluster 方案提供了一种重定向机制,所谓的“重定向”,就是指,客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,客户端要再给一个新实例发送操作命令。

那客户端又是怎么知道重定向时的新实例的访问地址呢?当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。

11 “万金油”的String,为什么不好用了?

为什么 String 类型内存开销大?

  • buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
  • len:占 4 个字节,表示 buf 的已用长度。
  • alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。

RedisObject 包含了 8 字节的元数据和一个 8 字节指针

内存碎片

哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节

jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。

用什么数据结构可以节省内存?

Redis 有一种底层数据结构,叫压缩列表(ziplist),这是一种非常节省内存的结构。

我们先回顾下压缩列表的构成。表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。

压缩列表之所以能节省内存,就在于它是用一系列连续的 entry 保存数据。每个 entry 的元数据包括下面几部分。

  • prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。
  • len:表示自身长度,4 字节;
  • encoding:表示编码方式,1 字节;
  • content:保存实际数据。

如何用集合类型保存单值的键值对?

在保存单值的键值对时,可以采用基于 Hash 类型的二级编码方法。这里说的二级编码,就是把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为 Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了。

Hash 类型设置了用压缩列表保存数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保存数据了。

12 有一亿个keys要统计,应该用哪种集合?

聚合统计

1
SINTERSTORE user:id:rem user:id:20200803 user:id:20200804

你可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计

排序统计

Sorted Set

二值状态统计

二值状态统计。这里的二值状态就是指集合元素的取值就只有 0 和 1 两种。

Bitmap

第一步,执行下面的命令,记录该用户 8 月 3 号已签到。

1
SETBIT uid:sign:3000:202008 2 1 

第二步,检查该用户 8 月 3 日是否签到。

1
GETBIT uid:sign:3000:202008 2 

第三步,统计该用户在 8 月份的签到次数。

1
BITCOUNT uid:sign:3000:202008

10 天连续签到的用户总数

Bitmap 支持用 BITOP 命令对多个 Bitmap 按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中。

基数统计

网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次

HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。

HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。

13 GEO是什么?还可以定义新的数据类型吗?

面向 LBS 应用的 GEO 数据类型

14 如何在Redis中保存时间序列数据?

基于 Hash 和 Sorted Set 保存时间序列数据

Hash 类型有个短板:它并不支持对数据进行范围查询。

Sorted Set 范围查询

16 异步机制:如何避免单线程模型的阻塞?

Redis 实例有哪些阻塞点?

1. 和客户端交互时的阻塞点

作的复杂度是否为 O(N) 第一个阻塞点:集合全量查询和聚合操作

如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞。

**bigkey 删除操作就是 Redis 的第二个阻塞点

第三个阻塞点:清空数据库**

2. 和磁盘交互时的阻塞点

AOF 日志时,会根据不同的写回策略对数据做落盘保存

3. 主从节点交互时的阻塞点

从库来说,它在接收了 RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库

加载 RDB 文件就成为了 Redis 的第五个阻塞点

4. 切片集群实例交互时的阻塞点

17 为什么CPU结构也会影响Redis的性能?

多核 CPU 架构下,Redis 如果在不同的核上运行,就需要频繁地进行上下文切换

建议你在 Redis 运行时,把实例和某个核绑定

18 波动的响应延迟:如何应对变慢的Redis?(上)

Redis 自身操作特性的影响

1. 慢查询命令

  1. 用其他高效命令代替。比如说,如果你需要返回一个 SET 中的所有成员时,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量数据,造成线程阻塞。
  2. 当你需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。

2. 过期 key 操作

**频繁使用带有相同时间参数的 EXPIREAT 命令设置过期 key,这就会导致,在同一秒内有大量的 key 同时过期。

19 波动的响应延迟:如何应对变慢的Redis?(下)

20 删除数据后,为什么内存占用率还是很高?

当数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统

Redis 释放的内存空间可能并不是连续的,那么,这些不连续的内存空间很有可能处于一种闲置的状态。这就会导致一个问题:虽然有空闲空间,Redis 却无法用来保存数据,不仅会减少 Redis 能够实际保存的数据量,还会降低 Redis 运行机器的成本回报率。

什么是内存碎片?

内存碎片是如何形成的?

内因:内存分配器的分配策略

Redis 可以使用 libc、jemalloc、tcmalloc 多种内存分配器来分配内存,默认使用 jemalloc

jemalloc 的分配策略之一,是按照一系列固定的大小划分内存空间,例如 8 字节、16 字节、32 字节、48 字节,…, 2KB、4KB、8KB 等。当程序申请的内存最接近某个固定值时,jemalloc 会给它分配相应大小的空间。

外因:键值对大小不一样和删改操作

这些键值对会被修改和删除,这会导致空间的扩容和释放。具体来说,一方面,如果修改后的键值对变大或变小了,就需要占用额外的空间或者释放不用的空间。另一方面,删除的键值对就不再需要内存空间了,此时,就会把空间释放出来,形成空闲空间。

如何判断是否有内存碎片?

Redis 自身提供了 INFO 命令,可以用来查询内存使用的详细信息,命令如下:

1
2
3
4
5
6
7
8
INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86

如何清理内存碎片?

  • info memory 命令是一个好工具,可以帮助你查看碎片率的情况;
  • 碎片率阈值是一个好经验,可以帮忙你有效地判断是否要进行碎片清理了;
  • 内存碎片自动清理是一个好方法,可以避免因为碎片导致 Redis 的内存实际利用率降低,提升成本收益率。

21 缓冲区:一个可能引发“惨案”的地方

23 旁路缓存:Redis是如何工作的?

24 替换策略:缓存满了怎么办?

Redis 缓存有哪些淘汰策略?

25 缓存异常(上):如何解决缓存和数据库的数据不一致问题?

情况一:先删除缓存,再更新数据库。

情况二:先更新数据库值,再删除缓存值。

  • 删除缓存值或更新数据库失败而导致数据不一致,你可以使用重试机制确保删除或更新操作成功。
  • 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删。

26 缓存异常(下):如何解决缓存雪崩、击穿、穿透难题?

缓存雪崩

大量的应用请求无法在 Redis 缓存中进行处理

EXPIRE 命令给每个数据设置过期时间时,给这些数据的过期时间增加一个较小的随机数

是在业务系统中实现服务熔断或请求限流机制。

我给你的第二个建议就是事前预防。

过主从节点的方式构建 Redis 缓存高可靠集群

缓存击穿

热点数据 jvm内存

缓存穿透

第一种方案是,缓存空值或缺省值。

第二种方案是,使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。

图中布隆过滤器是一个包含 10 个 bit 位的数组,使用了 3 个哈希函数,当在布隆过滤器中标记数据 X 时,X 会被计算 3 次哈希值,并对 10 取模,取模结果分别是 1、3、7。所以,bit 数组的第 1、3、7 位被设置为 1。当应用想要查询 X 时,只要查看数组的第 1、3、7 位是否为 1,只要有一个为 0,那么,X 就肯定不在数据库中。

27 缓存被污染了,该怎么办?

那什么是缓存污染呢?在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。

LRU 缓存策略

edis 中的 LRU 策略,会在每个数据对应的 RedisObject 结构体中设置一个 lru 字段,用来记录数据的访问时间戳。在进行数据淘汰时,LRU 策略会在候选数据集中淘汰掉 lru 字段值最小的数据(也就是访问时间最久的数据)。

LFU 缓存策略的优化

LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。

Redis 在实现 LFU 策略的时候,只是把原来 24bit 大小的 lru 字段,又进一步拆分成了两部分

  1. ldt 值:lru 字段的前 16bit,表示数据的访问时间戳;
  2. counter 值:lru 字段的后 8bit,表示数据的访问次数。

在实现 LFU 策略时,Redis 并没有采用数据每被访问一次,就给对应的 counter 值加 1 的计数规则,而是采用了一个更优化的计数规则

简单来说,LFU 策略实现的计数规则是:每当数据被访问一次时,首先,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。

下面这段 Redis 的部分源码,显示了 LFU 策略增加计数器值的计算逻辑。其中,baseval 是计数器当前的值。计数器的初始值默认是 5(由代码中的 LFU_INIT_VAL 常量设置),而不是 0,这样可以避免数据刚被写入缓存,就因为访问次数少而被立即淘汰。

1
2
3
4
double r = (double)rand()/RAND_MAX;
...
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;   

使用了这种计算规则后,我们可以通过设置不同的 lfu_log_factor 配置项,来控制计数器值增加的速度,避免 counter 值很快就到 255 了。

设计了一个 counter 值的衰减机制。

简单来说,LFU 策略使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。

28 Pika:如何基于SSD实现大容量Redis?

SSD 来实现大容量的 Redis 实例。360 公司 DBA 和基础架构组联合开发的 Pika键值数据库

29 无锁的原子操作:Redis如何应对并发访问?

Redis 的两种原子操作方法

  • 把多个操作在 Redis 中实现成一个操作,也就是单命令操作;INCR/DECR 命令
  • 把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。

别担心,Redis 提供了 INCR/DECR 命令,把这三个操作转变为一个原子操作了。INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作,Redis 在执行它们时,本身就具有互斥性。

30 如何使用Redis实现分布式锁?

1
SET key value [EX seconds | PX milliseconds]  [NX]
1
2
3
4
5
6
//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Lua 脚本

基于多个 Redis 节点实现高可靠的分布式锁

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。

第一步是,客户端获取当前时间。

第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作。

这里的加锁操作和在单实例上执行的加锁操作一样,使用 SET 命令,带上 NX,EX/PX 选项,以及带上客户端的唯一标识。当然,如果某个 Redis 实例发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,我们需要给加锁操作设置一个超时时间。

如果客户端在和一个 Redis 实例请求加锁时,一直到超时都没有成功,那么此时,客户端会和下一个 Redis 实例继续请求加锁。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。

第三步是,一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

  • 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;
  • 条件二:客户端获取锁的总耗时没有超过锁的有效时间。

在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。

当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作。

在 Redlock 算法中,释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。这样一来,只要 N 个 Redis 实例中的半数以上实例能正常工作,就能保证分布式锁的正常工作了。

31 事务机制:Redis能实现ACID属性吗?

Redis 的事务机制能保证哪些属性?

原子性

  • 命令入队时就报错,会放弃事务执行,保证原子性;
  • 命令入队时没报错,实际执行时报错,不保证原子性;
  • EXEC 命令执行时实例故障,如果开启了 AOF 日志,可以保证原子性。

一致性

在命令执行错误或 Redis 发生故障的情况下,Redis 事务机制对一致性属性是有保证的。

隔离性

基于WATCH机制

持久性

所以,不管 Redis 采用什么持久化模式,事务的持久性属性是得不到保证的。

Redis 的事务机制可以保证一致性和隔离性,但是无法保证持久性。

32 Redis主从同步与故障切换,有哪些坑?

33 脑裂:一次奇怪的数据丢失

第一步:确认是不是数据同步出现了问题

在主从集群中发生数据丢失,最常见的原因就是主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。

第二步:排查客户端的操作日志,发现脑裂现象

第三步:发现是原主库假故障导致的脑裂

在切换过程中,既然客户端仍然和原主库通信,这就表明,原主库并没有真的发生故障(例如主库进程挂掉)。我们猜测,主库是由于某些原因无法处理请求,也没有响应哨兵的心跳,才被哨兵错误地判断为客观下线的。结果,在被判断下线之后,原主库又重新开始处理请求了,而此时,哨兵还没有完成主从切换,客户端仍然可以和原主库通信,客户端发送的写操作就会在原主库上写入数据了。

35 Codis VS Redis Cluster:我该选择哪一个集群方案?

36 Redis支撑秒杀场景的关键技术和实践都有哪些?

基于原子操作支撑秒杀场景

1
2
key: itemID
value: {total: N, ordered: M}

其中,itemID 是商品的编号,total 是总库存量,ordered 是已秒杀量。

1
2
3
4
5
6
7
8
9
10
11
12
#获取商品库存信息        
local counts = redis.call("HMGET", KEYS[1], "total", "ordered");
#将总库存转换为数值
local total = tonumber(counts[1])
#将已被秒杀的库存转换为数值
local ordered = tonumber(counts[2])  
#如果当前请求的库存量加上已被秒杀的库存量仍然小于总库存量就可以更新库存     
if ordered + k <= total then
    #更新已秒杀的库存量
    redis.call("HINCRBY",KEYS[1],"ordered",k)                              return k;  
end           
return 0

基于分布式锁来支撑秒杀场景

使用分布式锁来支撑秒杀场景的具体做法是,先让客户端向 Redis 申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减

37 数据分布优化:如何应对数据倾斜?

数据量倾斜的成因和应对方法

bigkey 导致倾斜

一个根本的应对方法是,我们在业务层生成数据时,要尽量避免把过多的数据保存在同一个键值对中。

bigkey 正好是集合类型,我们还有一个方法,就是把 bigkey 拆分成很多个小的集合类型数据,分散保存在不同的实例上。

Slot 分配不均衡导致倾斜

集群运维人员没有均衡地分配 Slot

Hash Tag 导致倾斜

数据访问倾斜的成因和应对方法

热点数据既有多个副本可以同时服务请求