子弹短信内部技术分享:Redis

原理

Redis 是一个内存型「数据库」,除存储以外,它还有许多强大的命令,使之远远超出了数据库的定义,因此官方称之为「data structure store」,数据结构存储系统。 经过 Redis 提供的指令,咱们能够实现缓存、消息队列、事件通知、排行榜、库存管理、分布式锁等功能。node

基础结构

Redis 核心是单进程单线程服务,经过 epoll、select 等实现了 IO 多路复用,能够并发处理网络事件。redis

数据结构

Redis 提供了如下几种典型的数据结构算法

strings

Redis 实现了名为 SDS(Simple Dynamic String) 的字符串类型,与 C 字符串区别:spring

  1. 实现字符串拼接,减小内存重分配
  2. 维护了字符串的长度,以便快速获取及避免缓冲区溢出
  3. 二进制安全,即支持存储空格(\0)

linkedlist

Redis 实现了双向无环链表,并使用此数据结构实现了 list。sql

Hashtable

Redis 实现了符合自身使用场景的 HashMap,即数组加链表的实现。此数据结构实现了 Redis 中的 Hash、Set 数据类型。特色以下:数据库

  1. 使用 MurmurHash3 Hash 算法,针对规律性强的字符串有更好分布性。
  2. 新节点插入到表头而非表尾,由于缓存必定程度上会存在,「后加入的缓存会比先前加入的缓存更容易被访问」的特色。
  3. 渐进式 rehash。Redis 数据库自己是个巨大的 Hash 表,每次 rehash 要操做几百上千万的 key,渐进式 rehash 则是其中必不可少的保障。 rehash 的方式是维护两张表和索引,须要 rehash 时将 rehashIndex 置为 0,而后每次除 insert 操做外,都会将 oldTable 的 rehashIndex 中数据转移到 newTable 中,直到 rehashIndex == oldTable.length() - 1,再将 rehashIndex 置为 -1,rehash 完成。

skiplist

跳跃表经过给链表分层,实现了平均 O(logN),最坏 O(N) 的时间复杂度。Redis 使用该数据结构实现了 Sorted Set 数据类型。另外 Sorted Set 中还须要使用 HashTable 来实现 O(1) 的查询。json

intset

整数集合,即只保存整数的集合。Redis 使用该数据结构实现了 Set。数组

ziplist

压缩列表。压缩列表是一种牺牲性能节约空间的数据结构,相比链表,它节约了指针的空间,Redis 将它做为 List、Hash、Sorted Set 的实现,并使用 hash-max-ziplist-entries(512)、hash-max-ziplist-value(64)、list-max-ziplist-size(8 Kb)、zset-max-ziplist-entries(128)、zset-max-ziplist-value(64) 配置来决定是否使用 ziplist。缓存

持久化

不管是内存型的数据库仍是关系型数据库,宕机、停电后数据没法恢复都是不可接受的。Redis 有两种备份数据的方式:安全

AOF

即 Append-Only-File,当开启备份时,Redis 会建立出一个默认名称为 appendonly.aof 的文件。并将内存中全部数据以命令的形式写入文件中,后续执行新的操做数据的命令时,会放入缓冲区中定时写入文件(appendfsync 不为 always 时)。 在 redis.conf 中用如下参数配置 AOF 策略:

appendonly yes/no 是否开启 AOF 模式
appendfilename appendonly.aof
appendfsync always/everysec/no #写入磁盘时机,always 表示每次都会同步到磁盘,因为是同步操做,性能降低严重。everysec 表示每秒刷盘。no 表示只放入缓存区中,由操做系统指定刷盘时机(Linux 通常是 30 秒)
复制代码

当我执行了如下命令时:

set liuzhiguo 123
set liuzhiguo abc
set liuzhiguo 456
set liuzhiguo 1231 ex 30
复制代码

AOF 文件长这样:

*2 		消息行数
$6 		第一条消息长度
SELECT  消息内容
$1 		第二条消息长度
0		消息内容

*3		
$3
set
$9
liuzhiguo
$3
123

*3
$3
set
$9
liuzhiguo
$3
abc

*3
$3
set
$9
liuzhiguo
$3
456

*3
$3
set
$9
liuzhiguo
$4
1231

*3
$9
PEXPIREAT
$9
liuzhiguo
$13
1544420872751
复制代码

能够看出 AOF 模式是直接将命令写入文件中,因此在恢复数据时,Redis 会逐条执行命令来恢复数据。因此 AOF 模式恢复数据的效率并不高,并且当重复对一个 key 进行操做时,也须要执行全部操做命令。 针对同一数据重复操做的问题,Redis 提供了 AOF 重写的功能,即丢弃原有的 appendonly.aof 文件,从新将内存中的数据做为命令写入文件中。

RDB

即 Redis DataBase,此持久化模式默认开启。 开始备份时,Redis 会 fork 出一个子进程(bgsave),建立默认名为 dump.rdb 的二进制文件,逐个对内存中的数据进行备份。每次备份时都会抛弃原有的 RDB 文件,从新将数据全量备份。 对于备份的时机,在 redis.conf 有如下选项来触发备份:

save 900 1		900 秒内有 1 次变更
save 300 10		300 秒内有 10 次变更
save 60 10000	60 秒内有 10000 次变更
复制代码

RDB 因为体积和自然的指令压缩能力,恢复数据速度要大大快于 AOF。可是由于每次只能全量备份,资源消耗比 AOF 大,不如 AOF 灵活。而且由于备份时机的不肯定性,数据完整不如 AOF。

RDB-AOF

Redis 在 4.0 以后提出了 RDB-AOF 混合模式持久化,能够在 redis.conf 中经过 aof-use-rdb-preamble 选项开启。 此模式下,全量备份、重写 AOF 时会使用 RDB 格式,随后执行命令仍是以 AOF 的格式追加到文件中。

这样一来,恢复数据时性能比单纯 AOF 强,全量备份比 AOF 快,备份体积比 AOF 小,部分备份性能比 RDB 高。

高可用

Redis 经过哨兵(Sentinel)与复制的方式实现了高可用

复制

经过在 redis.conf 文件中配置「slaveof ip port」或给运行中的 redis 节点执行命令「slaveof ip port」,便可使得该节点成为某个 redis 实例的从节点。

从节点(slave)启动时会向主节点(master)发送 sync 指令,主节点使用 bgsave 方法生成 RDB 文件,并创建缓冲区记录写命令。RDB 文件生成会即发送给从节点,从节点开始载入 RDB 文件,此动做同步执行。 从节点完成载入后,主服务器会将缓冲区的记录发送给从服务器,此后主节点每当有执行命令时,都会传播给从节点一份。

断线重连后,从节点再次上线时会向主节点发送 psync 命令执行部分重同步,主节点会将此期间的命令发送给从节点执行。为实现此功能,主从节点维护了「复制偏移量」。

使用 info 能够查看复制的状态:

# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=280,lag=0		// 从节点信息
master_replid:6088224db78515c7c2cbef387fb90cefd459f0d5
master_repl_offset:280											// 主节点偏移量
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:280


# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1									// 与主节点 1 秒前同步
master_sync_in_progress:0										// 是否在进行 sync 同步
slave_repl_offset:280											// 从节点偏移量
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:6088224db78515c7c2cbef387fb90cefd459f0d5
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:280
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:280

复制代码

Sentinel

为实现高可用,只有复制是不够的,还须要主节点服务不可用后,从节点能自动补位。 Redis 经过 Sentinel 来实现节点监控与协调,Sentinel 是一个特殊的 Redis 节点,须要启动时指定参数 --sentinel 和 sentinel.conf 配置文件,并在配置文件中指定主节点的 ip、host。 Sentinel 启动后会向主节点发送 info 命令,获取到相应的从节点信息,并与从节点创建链接。 当主节点不响应时,Sentinel 会等待至配置中指定的 timeout 时间,随后将从节点提高为主节点。主节点再次启动时,Sentinel 会向主节点发送 slaveof 命令,要求其成为从节点。

Sentinel 自己一样支持高可用,多个 Sentinel 会向每一个主从节点 publish 本身的信息,以此来得知其余 Sentinel 的存在并创建链接。多个 Sentinel 共存时,对主从节点状态、身份的共识会有更复杂的协调过程,这就是另一个漫长的故事了。

对 Sentinel 的详细介绍,能够见:https://redis.io/topics/sentinel ,以及参考《Redis 设计与实现(第二版)》.

集群

Redis 由于是内存型数据库,在存储空间上容易捉襟见肘,因而产生了许多扩容方案。

客户端分片

如 ShardedJedis,经过在客户端对 key 进行 hash,再分给指定的节点。 优势:无需改动 Redis 便可扩容 缺点:只能扩容一次,没法平滑升级

代理层分片

如 Twemproxy。代理层接收客户端的请求,代理到对应的 Redis 节点上,一般也是使用一致性 hash 来分片。并因为代理层能够统一配置或读取同一数据源,作到可拓展代理层。 优势:客户端无需关心 Redis 服务状态,也无需分片。 缺点:难以扩容。

Redis Cluster

Redis 本身实现的集群,可实现无痛扩容,平滑迁移。启动集群模式须要在配置文件中配置:

cluster-enabled yes
cluster-config-file nodes.conf 
cluster-node-timeout 1500
复制代码

集群模式下,会建立出 16384 个槽,并给集群中每一个节点分配本身的槽数,槽必须被所有指定才能工做,一个节点最低指定一个槽。因此 Redis 集群理论上最大是 16384 个节点。

当须要添加/获取某个 key 时,经过 crc16(key) & 16384 获得这个 key 应在的槽,随后找出这个槽所在的节点,若是节点是本身直接执行,不然会返回给客户端对应的节点的 ip + port。

Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通讯,集群的消息有如下几种类型:

  1. Meet。经过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。
  2. Ping。节点每秒会向集群中其余节点发送 ping 消息,消息中带有本身已知的两个节点的地址、槽、状态信息、最后一次通讯时间等。
  3. Pong。节点收到 ping 消息后会回复 pong 消息,消息中一样带有本身已知的两个节点信息。
  4. Fail。节点 ping 不通某节点后,会向集群全部节点广播该节点挂掉的消息。其余节点收到消息后标记已下线。

因为去中心化和通讯机制,Redis Cluster 选择了最终一致性和基本可用。例如当加入新节点时(meet),只有邀请节点和被邀请节点知道这件事,其他节点要等待 ping 消息一层一层扩散。除了 Fail 是当即全网通知的,其余诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都须要等待被通知到。

所以,因为 gossip 协议,Redis Cluster 对服务器时间的要求较高,不然时间戳不许确会影响节点判断消息的有效性。另外节点数量增多后的网络开销也会对服务器产生压力。所以官方推荐最大节点数为 1000。对于 Redis 集群的运维,能够参考 优酷蓝鲸近千节点的 Redis 集群运维经验总结

优势:

  1. 真正的弹性扩容缩容。
  2. 扩容期间不影响使用。

缺点:

  1. 缺少管理平台。
  2. 客户端要另作兼容。
  3. 部分命令不支持

Redis 经过 Cluster 解决了扩容以后,客户端该怎么使用呢? 如 JedisCluster,每次请求前会拉取节点的 cluster info 来计算应该到哪一个节点请求,并须要对错误节点返回的 ASK 消息作相应的处理。由此产生的问题是

  1. 每次操做最少请求两次。
  2. 每次若是只请求某一个节点,也会造成单点压力。

对问题 1,解决办法是客户端缓存集群状态。对问题 2,JedisCluster 支持配置多个节点,拉取节点信息时会随机选择某节点以分摊压力。对问题 2 的处理方式,须要将 Redis 节点信息同步到客户端配置中,产生了耦合。

另外的问题是,集群状态下是不支持 mget、mset 等须要跨节点执行的命令。该问题的解决方案是加一层 Proxy,推荐 优酷土豆的Redis服务平台化之路,其使用 Nginx + Redis Cluster 的思路使人赞叹,并用请求聚合的方式实现了跨节点执行命令的问题。

阿里云提供的 Redis 服务一样实现了集群模式下的跨节点命令,采用代理 + 分片服务器 + 分片配置服务器(极可能是 zookeeper),可是没有使用 Redis Cluster 机制,而是本身实现的「分片」,保留了 slot。阿里云的 Redis 好处是集群版无需客户端作兼容,能够当成单机 Redis 使用,出了问题方便甩锅。

第三方魔改 Redis

在等待 Redis 出官方集群方案以前,人们火烧眉毛想要集群版的 Redis,一些不满于现状以及不满于 Redis Cluster 实现的人们开始对 Redis 进行改造。前面提到的阿里云 Redis 也属于魔改后的 Redis。

Codis

Codis 几乎是最知名的第三方 Redis,对 Redis 进行了大量改造。 其架构为 zookeeper + proxy + server-group(master + slave),并提供了控制台以即可视化运维。

经过 zookeeper 记录可用的 proxy 节点,再使用 Codis 开发组基于 Jedis 修改的 Jodis 客户端到 zookeeper 中寻找可用的 proxy 节点进行调用。若是使用的是 jedis 或其余客户端,则只能到链接一个 proxy,或者想办法链接到 zookeeper 获取节点,再进行轮询调用。

Codis 支持弹性扩容,分片方式与 Redis Cluster 相似,经过 crc32(key) % 1024 分红 1024 个槽,每台实例保存对应槽的数据。

LedisDB 和 SSDB

LedisDB 和 SSDB 很是类似,都是用 LevelDB 底层,从新实现了 Redis,或者说只实现了 Redis 协议。经过多线程 + 硬盘的方式,实现了和单机 Redis 类似的 QPS 性能,并能够很大程度上对容量进行扩容。 LedisDB/SSDB 与 Redis 的关系,至关于 TiDB 与 MySQL 的关系。

缺点是出了容量上的成本优点,其余没有任何优点。

事务

Redis 提供 watchmultiexec 等方法实现乐观锁事务。使用事务的流程以下:

  1. watch key1 key2
  2. multi 开启事务
  3. set key1 value一、set key2 value2,将指令入队。
  4. exec,执行指令。

若是 multi ~ exec 之间 key1/key2 被其余客户端修改过,exec 时会返回 nil, set key1 value一、set key2 value2 均不会执行。 Redis 会保存一个 watch_keys 字典,结构为: client -> keys、is_dirty。Redis 在处理每个会修改数据的命令时,会检查 watch_keys 是否存在该 key,若是有,则修改 is_dirty 为 true。

执行事务的客户端在执行 exec 时,会检查 is_dirty 字段,若是发现为 false,全部的积累的指令会直接丢弃不执行。

事务在 Redis 中的使用场景很少,并发量大的状况下须要反复重试,大部分状况下有更好的使用方式:

Lua

Redis 提供了对 Lua 脚本的支持,原子性执行一系列指令,并能够写代码作逻辑判断。 例如须要大量插入数据的场景:

for i=1,10000000,1 do
    local num = math.random(1000000,999999999);
    redis.call("set",num,i)
end
复制代码

执行一千万条命令在本机大概用了 12 秒,QPS 83w。 Redis 在执行 Lua 脚本时是单线程,没法处理其余请求,这也是 Redis 原子性的缘由。下面是抢红包时利用该特性实现的 Lua 脚本:

// 该脚本传入 4 个参数
// KEYS[1] = 未领取的红包列表 key
// KEYS[2] = 已领取的红包列表 key
// KEYS[3] = 红包已领取人ID列表 key
// KEYS[4] = 领取人ID

// 检查领取人是否在已领取列表内
if redis.call('hexists', KEYS[3], KEYS[4]) ~= 0 then
	return nil
else
	// 取出一个未领取的红包
	local redEnvelop = redis.call('rpop', KEYS[1]);
	if redEnvelop then
		// 红包中的 receiver 填入领取人 ID
		local x = cjson.decode(redEnvelop);
		x['receiver'] = KEYS[4];
		local re = cjson.encode(x);

		// 领取人放入已领取人ID列表,将红包放入已领取红包列表
		redis.call('hset', KEYS[3], KEYS[4], KEYS[4]);
		redis.call('lpush', KEYS[2], re);

		// 给相应的 key 续期
		if redis.call('llen', KEYS[2]) == 1 then
			redis.call('expire', KEYS[2], 172800);
		end
		if redis.call('hlen', KEYS[3]) == 1 then
			redis.call('expire', KEYS[3], 172800);
		end
		return re;
	end
end
return nil
复制代码

须要注意的是,因为 Lua 脚本只能在单个 Redis 实例执行,因此在集群状态下执行 Lua 时,Redis 会对要执行的 key 进行检查。为了保证全部 key 必定在某一台机器上,Redis 限制了全部 key 都必须在同一个 slot 内才行。

因此针对红包的场景,对 Lua 中传入的 key 作了xxx{redpacketId}的处理,以保证全部 key 落在一个 slot 上。

管道(pipeline)

Redis 支持使用管道批量执行命令,再统一返回,减小往返次数,一般用于批量插入数据,批量获取数据。

实战

缓存

缓存方式

缓存是 Redis 最多见的场景。一般缓存的过程为:

  1. 未命中:从数据源中取得数据,放入缓存中。
  2. 命中:返回数据。
  3. 更新:先把数据存入数据库,再使缓存失效。

不推荐更新数据时同时更新到缓存,由于可能并发更新致使脏数据。见 为何 Facebook 删除缓存而不是更新缓存? 以及 Scaling Memcache at Facebook,其中提到「We choose to delete cached data instead of updating it because deletes are idempotent」。 但删除缓存并非彻底不会致使脏数据,只是几率会相对小不少。

批量查询

查询时可能会须要相似 where id in (xx,yy,zz) 的状况,这时查询缓存可使用 mget 同时查询多个 key,能够大大提升效率。下面是 benchmark 数据:

get 81833.06 requests per second	
mget 10 73475.39 requests per second		  734,753
mget 20 64226.07 requests per second		  642,260			
mget 30 59559.26 requests per second		1,786,770			99%   < 1 milliseconds
mget 50 48995.59 requests per second		2,449,750			99%   < 1.5 milliseconds
mget 100 29214.14 requests per second		2,921,414			99%   < 2.5 milliseconds
mget 200 16730.80 requests per second		3,346,000			99%   < 3 milliseconds
mget 500 7222.30 requests per second		3,611,150			99%   < 9 milliseconds
复制代码

根据总获取数据个数、平均响应时间,一般认为 mget 数量控制在 100 如下是比较均衡的。

按每次 mget 100 与 get 相比,性能至关于提升了 35 倍。再加上跨机器调用往返的时间消耗,实际状况性能提高极可能 100 倍以上。

分布式锁

Redis 能够经过 SET key randomValue NX EX 30 给某个 key 赋值,并同时判断 key 是否存在,以及给定过时时间。过时时间要根据业务变化。

释放锁能够直接 del 掉这个 key。可是 del 是有风险的:

例如 A 获取到锁,过时时间 30 秒。由于某些缘由 30 秒没能处理完请求,B 过来也获取到了锁。此时 A 处理完执行释放锁的操做,就会释放掉 B 所持有的锁。

为了不这个问题,须要判断 value 是否是 set 时的 value,若是是才执行 del 操做。为了让这两条命令原子性执行,须要使用到 lua 脚本:

- KEYS[1] 为 锁名称,ARGV[1] 为锁内容, 即 set 时的 randomValue
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
复制代码

另外在 spring-data-redis 的实现中,是没有 set nx ex 的,因此须要找到 Jedis 或者 Lettuce 调用原生方法。

计数器

Redis 另外一个值得称道的命令就是自增了,其提供了 incr/incrby/incrbyfloat(string)、hincrby/hincr(hash)、zincrby(zset)方法供不一样数据类型使用。

经过这些命令能够实现对库存的扣减,记录接口访问频次,记录一篇文章的点赞数、评论数、转发数,抢红包扣减数量等。

排行榜

利用 zset 有序列表,好比要计算用户积分排行榜:

  • zadd/zincrby 保存或自增用户的积分
  • zrevrank 获取用户的排名
  • zscore 获取用户的积分
  • zrevrange 获取排行

消息队列

利用 list 的 lpush(Left Push) 和 brpop(Blocked Right Pop) 接口能够实现消息队列功能:

  1. 将消息 lpush 到队列中。
  2. 全部实例经过 brpop 监听队列并取出数据进行消费。

在消费的过程当中能够经过配置线程池,根据业务状况决定消费速率。

异步延迟合并队列

好比秒杀、抢红包时,库存数据须要异步入库。但仅仅异步入库也是不够的,并不会减小对数据库操做的次数。这时候可能须要将 100 次请求压缩成一次请求,只取最后的数据落库。

此类需求则能够用 zset + list 实现,咱们须要几个东西:

  1. 须要延迟执行的任务放入 zset 列表中,score 为须要执行的时间戳。
  2. 后台起一个线程每秒钟拉取 zset ,执行 zrangeByScore, score 范围为 0 ~ 当前时间戳,若是取到数据则放入执行队列 list 中,最后 zrem 查出来的数据。
  3. 监听 list 队列的执行器,此时开始执行任务。

这也是红包中的异步更新的实现方式。在抢群红包时,若是每次都更新数据库中的数据,势必会增长响应时间。使用这种更新方式的话,只在最后一次抢红包的 30s 后更新,30s 以内发生的数据更新,都只会合并为 1 条。

事件通知

Redis 提供了publishsubscribe 等命令实现了广播功能,publish 时能够将消息通知到某个频道(channel),此时 subscribe 了这个频道的节点均能收到消息。

经过这个机制咱们能作到对全节点的事件通知。

好比在积分系统中会将全部活动、抽奖、签到、摇钱树等数据库配置数据放入 JVM 缓存中,以便得到最高的性能。 为了更新数据,一开始是每分钟到数据库更新一次。但问题是每台实例更新的时机都不一样,致使请求到 A 实例的数据,与 B 实例上的不一样。随后将定时任务的配置改为了每分钟的第 0 秒执行,则很大程度上改善了问题。

可是轮询的方式仍然不够优雅,绝大部分时候取得的配置并无变化,是无用的请求。更新配置的时机应该是配置发生了变动才对。

这时就可使用 Redis 广播,每当数据库数据发生变化时,经过广播通知全部节点更新数据,或者干脆将要更新的数据放入广播中。

优化

Redis 虽然性能强悍,可是因为单线程的特性,一旦产生慢查询,会将全部操做都阻塞住。因此使用上仍须要注意会踩哪些坑。Redis 提供了 slowlog get 查看慢查询。

常见雷区

  • keys * keys 命令的时间复杂度是 O(n),n 是 Redis 中全部键的数量,这个是最多见的性能最差的命令。通常线上都把这个命令 block 掉(在配置中加 rename-command KEYS "")

  • 大 key。一个 key 里存储的数据越多,一般性能越差,好比对超大的 List 进行 lindex 和 lrange。另外大 value 在集群数据迁移时会阻塞可能致使 fail over。甚至在删除时也会阻塞,例如删除一个 1kw 数据量的 set,须要耗时 5s。或者在集群中大 key 会致使集群内存分配不均匀。因此在使用时须要避免在一个 key 中放入过多数据。

  • bgrewriteaofbgsave,重写 aof 文件及备份 RDB 文件时,会 fork 出子进程和内存,此期间是阻塞的,取决于 Redis 内存大小和机器性能。因此许多企业的作法是主节点上关闭 aof 和 rdb,只在从节点上备份。

大 key 的拆分

积分系统中存在一个进贡的任务,邀请人可得到被邀请人作任务的奖励,并在天天凌晨入帐。

对于这个任务,咱们作的第一步优化就是天天将得到了进贡奖励的用户,保存在 set 里,经过 sscan 遍历须要进贡的用户,执行任务。以此避免了扫库,保证每次取得的 userId 都是确切有效的。问题在于万一子弹短信火了,set 中的 userId 会原来越多,也就遇到了大 key 的问题,须要将 set 拆分为多个 set。

拆分的思路和 Redis 集群分片相似,经过 hash(userId) % count 的方式,获得 0 ~ count 之间的分片数,将其加到本来的 key 上,过程以下:

  1. 经过 hash(userID) % count,获得分片数,如 16
  2. 本来 key 为「TRIBUTE:USER:SET:20181225」,再加上分片数即获得「TRIBUTE:USER:SET:20181225:16」,再将 userId sadd 放入便可
  3. 取出全部 key 时 for 循环从 0-count 拼到 key 上,再针对每一个 key sscan。

使用 Hash

例如保存一篇文章的点赞数、转发数、评论数时,既能够保存为 3 个 value,即 article:like、article:repost、article:comment。也能够保存为一个 hash 对象,key 为 article,hashKey 为 like、repost、comment。

好处:

  1. 经过一条 hgetall 就能取得所需数据
  2. 节约内存。

使用 value :

# lua
for i=1,1000000,1 do
    redis.call("set","article:like:"..i,1)
    redis.call("set","article:repost:"..i,1)
    redis.call("set","article:comment:"..i,1)
end

# memory
used_memory:226568704
used_memory_human:216.07M
used_memory_rss:282144768
used_memory_rss_human:269.07M
复制代码

使用 hash :

# lua
for i=1,1000000,1 do
    redis.call("HMSET","article:"..i, "like", 1, "repost", 1, "comment", 1)
end

# memory
used_memory:121402896
used_memory_human:115.78M
used_memory_rss:132640768
used_memory_rss_human:126.50M
复制代码

value 几乎多使用了一倍内存。缘由是 hash 类型这时会选择 ziplist 数据结构实现。