共计 4065 个字符,预计需要花费 11 分钟才能阅读完成。
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