36 秒杀
秒杀场景可以分成秒杀前、秒杀中和秒杀后三个阶段。
主要特征:
- 瞬时并发高(数据库千级并发,Redis万级并发)
- 读多写少,读数据比较简单
秒杀过程:
1.秒杀前:
尽量把商品详情页的页面元素静态化,然后使用 CDN 或是浏览器把这些静态化的元素缓存起来。
无需使用Redis。
2.秒杀中:
这个阶段的操作就是三个:库存查验、库存扣减和订单处理,并发压力在库存查验上。
- 使用Redis保存库存量,这样一来,请求可以直接从 Redis 中读取库存并进行查验。
- 使用Redis进行库存扣减
- 使用数据库进行订单处理(保证事务)
注:为了避免请求查询到旧的库存值,库存查验和库存扣减这两个操作需要保证原子性(
lua脚本)。
3.秒杀结束后:
失败用户刷新商品详情,成功用户刷新订单详情。
无需使用Redis。
原子操作
在秒杀场景中,一个商品的库存对应了两个信息,分别是总库存量和已秒杀量。
其中,itemID 是商品的编号,total 是总库存量,ordered 是已秒杀量。
key: itemID
value: {total: N, ordered: M}
方法1:lua脚本
#获取商品库存信息
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
方法2:分布式锁
先让客户端向 Redis 申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减。
//使用商品ID作为key
key = itemID
//使用客户端唯一标识作为value
val = clientUniqueID
//申请分布式锁,Timeout是超时时间
lock =acquireLock(key, val, Timeout)
//当拿到锁后,才能进行库存查验和扣减
if(lock == True) {
//库存查验和扣减
availStock = DECR(key, k)
//库存已经扣减完了,释放锁,返回秒杀失败
if (availStock < 0) {
releaseLock(key, val)
return error
}
//库存扣减成功,释放锁
else{
releaseLock(key, val)
//订单处理
}
}
//没有拿到锁,直接返回
else
return
注意:使用分布式锁时,客户端需要先向 Redis 请求锁,只有请求到了锁,才能进行库存查验等操作,这样一来,客户端在争抢分布式锁时,大部分秒杀请求本身就会因为抢不到锁而被拦截。
建议:分布式锁和业务数据放在集群不同实例上,可以减轻业务数据实例压力。
小结
秒杀优化:
- 前端静态化:利用CDN和浏览器缓存。
- 请求拦截和控流:拦截恶意请求(黑名单),限制请求数量。
- 库存过期时间处理:不要设置过期时间,避免缓存击穿。
- 数据库订单异常处理:增加订单重试功能,保证订单成功处理。
建议:处理秒杀的业务数据用单独的实例保存,不要和日常业务放在一起。
问题:使用多个实例的切片集群来分担秒杀请求,是否是一个好方法?
优点:集群分担请求,可以降低单实例压力;
缺点:
- 请求不平均时会数据倾斜,增大单个实例压力,导致没有全部卖出;
- 获取库存时需要查询多个切片,这种情况建议不展示库存。
37 数据倾斜
数据倾斜有两类:
- 数据量倾斜:在某些情况下,实例上的数据分布不均衡,某个实例上的数据特别多。
- 数据访问倾斜:每个实例的数据量相差不大,但是某个实例上的数据是热点数据,被访问得非常频繁。
产生原因
数据倾斜的原因分别是某个实例上保存了 bigkey、Slot 分配不均衡以及 Hash Tag。
bigkey
- bigkey 的 value 值很大(String 类型),或者是 bigkey 保存了大量集合元素(集合类型),会导致这个实例的数据量增加,内存资源消耗也相应增加。
- bigkey 的操作一般都会造成实例 IO 线程阻塞,影响访问速度。
解决方法:
- 避免把过多数据放在一个key中
- 把集合类型的bigkey拆分成很多小集合,保存在不同实例上。
Slot分配不均衡
大量的数据被分配到同一个 Slot 中,而同一个 Slot 只会在一个实例上分布,这就会导致,大量数据被集中到一个实例上,造成数据倾斜。
查看Slot分配情况:
CLUSTER SLOTS
Slot迁移:
- CLUSTER SETSLOT:使用不同的选项进行三种设置,分别是设置 Slot 要迁入的目标实例,Slot 要迁出的源实例,以及 Slot 所属的实例。
- CLUSTER GETKEYSINSLOT:获取某个 Slot 中一定数量的 key。
MIGRATE:把一个 key 从源实例实际迁移到目标实例。
#1. 从实例 3 上迁入 Slot 300 CLUSTER SETSLOT 300 IMPORTING 3 # 2. 迁到实例5 CLUSTER SETSLOT 300 MIGRATING 5 # 3. 分批次迁移,一次100个key CLUSTER GETKEYSINSLOT 300 100 # 4. 执行迁移,设置数据库编号(0)和迁移超时时间 MIGRATE 192.168.10.5 6379 key1 0 timeout # 重复3,4步直到所有key迁移完成
Hash Tag
Hash Tag 是指加在键值对 key 中的一对花括号{}。
这对括号会把 key 的一部分括起来,客户端在计算 key 的 CRC16 值时,只对 Hash Tag 花括号中的 key 内容进行计算。
如果没用 Hash Tag 的话,客户端计算整个 key 的 CRC16 的值。
比如key为user:profile:{3231}时,只计算3231的CRC16值。
好处:HashTag相同时,数据会映射到同一个实例上。
应用场景:
主要是用在 Redis Cluster 和 Codis 中,支持事务操作和范围查询。因为 Redis Cluster 和 Codis 本身并不支持跨实例的事务操作和范围查询,当业务应用有这些需求时,就只能先把这些数据读取到业务层进行事务处理,或者是逐个查询每个实例,得到范围查询的结果。所以使用 Hash Tag 把要执行事务操作或是范围查询的数据映射到同一个实例上,这样就能很轻松地实现事务或范围查询了。
应对方法
- 如果热点数据以服务读操作为主,在这种情况下,我们可以采用热点数据多副本的方法来应对。
具体做法是,把热点数据复制多份,在每一个数据副本的 key 中增加一个随机前缀,让它和其它副本数据不会被映射到同一个 Slot 中。 - 对于有读有写的热点数据,我们就要给实例本身增加资源了,例如使用配置更高的机器,来应对大量的访问压力。
小结
建议:在构建切片集群时,尽量使用大小配置相同的实例(例如实例内存配置保持相同),这样可以避免因实例资源不均衡而在不同实例上分配不同数量的 Slot。
38 通信开销
Redis Cluster 的规模上限,是一个集群运行 1000 个实例。
实例间的通信开销会随着实例规模增加而增大,在集群超过一定规模时(比如 800 节点),集群吞吐量反而会下降。
Gossip 协议
Redis Cluster 在运行时,每个实例上都会保存 Slot 和实例的对应关系(也就是 Slot 映射表),以及自身的状态信息。
为了让集群中的每个实例都知道其它所有实例的状态信息,实例之间会按照一定的规则进行通信。这个规则就是 Gossip 协议。
- 每个实例之间会按照一定的频率,从集群中随机挑选一些实例,把 PING 消息发送给挑选出来的实例,用来检测这些实例是否在线,并交换彼此的状态信息。PING 消息中封装了发送消息的实例自身的状态信息、部分其它实例的状态信息,以及 Slot 映射表。
- 实例在接收到 PING 消息后,会给发送 PING 消息的实例,发送一个 PONG 消息。PONG 消息包含的内容和 PING 消息一样。
Gossip 协议可以保证在一段时间后,集群中的每一个实例都能获得其它所有实例的状态信息。
使用 Gossip 协议进行通信时,通信开销受到通信消息大小和通信频率这两方面的影响,消息越大、频率越高,相应的通信开销也就越大。
消息大小
1.对于一个包含了 1000 个实例的集群来说,每个实例发送一个 PING 消息时,会包含 100 个实例的状态信息,总的数据量是 10400 字节,再加上发送实例自身的信息,一个 Gossip 消息大约是 10KB。
2.为了让 Slot 映射表能够在不同实例间传播,PING 消息中还带有一个长度为 16,384 bit 的 Bitmap,这个 Bitmap 的每一位对应了一个 Slot,如果某一位为 1,就表示这个 Slot 属于当前实例。这个 Bitmap 大小换算成字节后,是 2KB。
把实例状态信息和 Slot 分配信息相加,就可以得到一个 PING 消息的大小了,大约是 12KB。每个实例发送了 PING 消息后,还会收到返回的 PONG 消息,两个消息加起来有 24KB。
通信频率
- Redis Cluster 的实例启动后,默认会每秒从本地的实例列表中随机选出 5 个实例,再从这 5 个实例中找出一个最久没有通信的实例,把 PING 消息发送给该实例。
- Redis Cluster 的实例会按照每 100ms 一次的频率,扫描本地的实例列表,如果发现有实例最近一次接收 PONG 消息的时间,已经大于配置项 cluster-node-timeout 的一半了(cluster-node-timeout/2),就会立刻给该实例发送 PING 消息,更新这个实例上的集群状态信息。
优化:
配置项 cluster-node-timeout 定义了集群实例被判断为故障的心跳超时时间,默认是 15 秒,可以调大到20~25秒。
可以在调整 cluster-node-timeout 值的前后,使用 tcpdump 命令抓取实例发送心跳信息网络包的情况。
tcpdump host 192.168.10.3 port 16379 -i 网卡名 -w /tmp/r1.cap
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。