diff --git a/notes/Redis.md b/notes/Redis.md index c26eaa72..c669b03c 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -2,6 +2,7 @@ * [Redis](#redis) * [一、概述](#一概述) + * [与 MySQL 的区别](#与-mysql-的区别) * [二、数据类型](#二数据类型) * [STRING](#string) * [LIST](#list) @@ -19,6 +20,7 @@ * [消息队列](#消息队列) * [会话缓存](#会话缓存) * [分布式锁实现](#分布式锁实现) + * [排行榜](#排行榜) * [其它](#其它) * [五、Redis 与 Memcached](#五redis-与-memcached) * [数据类型](#数据类型) @@ -31,10 +33,12 @@ * [RDB 持久化](#rdb-持久化) * [AOF 持久化](#aof-持久化) * [九、事务](#九事务) + * [Lua 脚本](#lua-脚本) * [十、事件](#十事件) * [文件事件](#文件事件) * [时间事件](#时间事件) * [事件的调度与执行](#事件的调度与执行) + * [单线程为什么快](#单线程为什么快) * [十一、复制](#十一复制) * [连接过程](#连接过程) * [主从链](#主从链) @@ -57,6 +61,15 @@ Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以 Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 +### 与 MySQL 的区别 + +- 数据位置:Redis 的数据全部在内存中,容量受内存大小限制;MySQL 的数据在磁盘上,内存中的缓冲池只缓存经常访问的页; +- 查询方式:Redis 只能按键访问,值是字符串、哈希、有序集合等数据结构;MySQL 用 SQL 按任意条件查询、联表和聚合,靠索引加速; +- 事务:MySQL 的事务满足 ACID,出错可以回滚;Redis 的事务只保证一组命令依次执行、不被其它客户端的命令插入,其中一条命令执行出错时,其它命令不会回滚,见 [九、事务](#九事务); +- 持久性:MySQL 默认在事务提交时把 redo log 刷到磁盘,提交成功的数据不会因为宕机丢失;Redis 即使开启 AOF 并设置为 everysec,宕机时也可能丢失最近一秒左右的写入。 + +数据都在内存中时,MySQL 按主键查询也很快。在本机用 50 个连接分别压测同一条主键查询和同一个键的 GET,各执行 20 万次:MySQL 每秒约 16 万次,用了约 6 个 CPU 核心,每次查询消耗约 36 微秒的 CPU 时间;Redis 每秒约 11 万次,只用了 1 个核心,每次约 8.7 微秒。Redis 的优势在于开销小、延迟稳定,以及计数器、排行榜这类 MySQL 不擅长的数据结构;需要持久保存、按多种条件查询的业务数据仍然放在 MySQL 中。两者通常一起使用,Redis 作为缓存,见 [缓存与数据库的一致性](缓存.md#缓存一致性)。 + ## 二、数据类型 | 数据类型 | 可以存储的值 | 操作 | @@ -67,146 +80,125 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中, | HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对\ 获取所有键值对\ 检查某个键是否存在| | ZSET | 有序集合 | 添加、获取、删除元素\ 根据分值范围或者成员来获取元素\ 计算一个键的排名 | -> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) +> [What Redis data structures look like](https://redis.io/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) ### STRING -

+

-```html -> set hello world +```text +> set user:1001:name alice OK -> get hello -"world" -> del hello +> get user:1001:name +"alice" +> set page:views 41 +OK +> incr page:views +(integer) 42 +> del user:1001:name (integer) 1 -> get hello +> get user:1001:name (nil) ``` ### LIST -

+

-```html -> rpush list-key item -(integer) 1 -> rpush list-key item2 -(integer) 2 -> rpush list-key item +```text +> rpush orders:queue 1024 1025 1026 (integer) 3 - -> lrange list-key 0 -1 -1) "item" -2) "item2" -3) "item" - -> lindex list-key 1 -"item2" - -> lpop list-key -"item" - -> lrange list-key 0 -1 -1) "item2" -2) "item" +> lpush orders:queue 1023 +(integer) 4 +> lrange orders:queue 0 -1 +1) "1023" +2) "1024" +3) "1025" +4) "1026" +> lindex orders:queue -1 +"1026" +> lpop orders:queue +"1023" +> lrange orders:queue 0 -1 +1) "1024" +2) "1025" +3) "1026" ``` ### SET -

+

-```html -> sadd set-key item -(integer) 1 -> sadd set-key item2 -(integer) 1 -> sadd set-key item3 -(integer) 1 -> sadd set-key item +```text +> sadd article:7:tags redis cache nosql redis +(integer) 3 +> smembers article:7:tags +1) "redis" +2) "cache" +3) "nosql" +> sismember article:7:tags mysql (integer) 0 - -> smembers set-key -1) "item" -2) "item2" -3) "item3" - -> sismember set-key item4 -(integer) 0 -> sismember set-key item +> sismember article:7:tags redis (integer) 1 - -> srem set-key item2 +> srem article:7:tags nosql (integer) 1 -> srem set-key item2 -(integer) 0 - -> smembers set-key -1) "item" -2) "item3" +> sadd article:8:tags redis mysql +(integer) 2 +> sinter article:7:tags article:8:tags +1) "redis" ``` ### HASH -

+

-```html -> hset hash-key sub-key1 value1 +```text +> hset user:1001 name alice age 25 city hangzhou +(integer) 3 +> hget user:1001 name +"alice" +> hincrby user:1001 age 1 +(integer) 26 +> hdel user:1001 city (integer) 1 -> hset hash-key sub-key2 value2 -(integer) 1 -> hset hash-key sub-key1 value1 -(integer) 0 - -> hgetall hash-key -1) "sub-key1" -2) "value1" -3) "sub-key2" -4) "value2" - -> hdel hash-key sub-key2 -(integer) 1 -> hdel hash-key sub-key2 -(integer) 0 - -> hget hash-key sub-key1 -"value1" - -> hgetall hash-key -1) "sub-key1" -2) "value1" +> hgetall user:1001 +1) "name" +2) "alice" +3) "age" +4) "26" ``` ### ZSET -

+

-```html -> zadd zset-key 728 member1 -(integer) 1 -> zadd zset-key 982 member0 -(integer) 1 -> zadd zset-key 982 member0 -(integer) 0 - -> zrange zset-key 0 -1 withscores -1) "member1" -2) "728" -3) "member0" -4) "982" - -> zrangebyscore zset-key 0 800 withscores -1) "member1" -2) "728" - -> zrem zset-key member1 -(integer) 1 -> zrem zset-key member1 -(integer) 0 - -> zrange zset-key 0 -1 withscores -1) "member0" -2) "982" +```text +> zadd rank:score 92 alice 85 bob 97 carol +(integer) 3 +> zrange rank:score 0 -1 withscores +1) "bob" +2) "85" +3) "alice" +4) "92" +5) "carol" +6) "97" +> zrange rank:score 0 1 rev withscores +1) "carol" +2) "97" +3) "alice" +4) "92" +> zrangebyscore rank:score 90 100 +1) "alice" +2) "carol" +> zincrby rank:score 10 bob +"95" +> zrange rank:score 0 -1 rev withscores +1) "carol" +2) "97" +3) "bob" +4) "95" +5) "alice" +6) "92" ``` ## 三、数据结构 @@ -335,11 +327,9 @@ int dictRehash(dict *d, int n) { 跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。 -

+在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 26 的过程。 -在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。 - -

+

与红黑树等平衡树相比,跳跃表具有以下优点: @@ -383,6 +373,22 @@ List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息 可以使用 SET 命令的 NX 和 PX 选项实现单实例的分布式锁,除此之外,还可以使用官方提出的 RedLock 算法在多个独立实例上实现分布式锁,详见 [分布式](分布式.md)。 +### 排行榜 + +有序集合按分值排序,正好用来实现排行榜:成员是文章或用户的 ID,分值是点赞数或销量。 + +```text +ZINCRBY rank:likes 1 article:1001 -- 点赞一次,分值加 1 +ZREVRANGE rank:likes 0 9 WITHSCORES -- 前 10 名 +ZREVRANK rank:likes article:1001 -- 第几名,从 0 开始 +``` + +在本机向一个有序集合写入 100 万个成员,占用约 105 MB 内存;查前 10 名、查某个成员的排名和加分都是 O(logN) 的操作,SLOWLOG 记录的服务器执行时间大多在 4~10 微秒之间。 + +分值相同时,Redis 按成员名的字典序排列,而业务上通常要求先达到这个分数的排在前面。可以把时间编进分值:分值 = 点赞数 × 1010 + (1010 − 达到这个点赞数的秒级时间戳)。例如 article:1001 和 article:1002 都是 520 个赞,时间戳分别为 1793000000 和 1793000300,分值为 5208207000000 和 5208206999700,先达到的 article:1001 排在前面。需要注意的是,分值是双精度浮点数,只能精确表示 253 以内的整数,这种写法下点赞数不能超过约 90 万,更大时要缩短时间部分的位数。 + +排行榜被大量用户同时查看时,前 10 名这个键会成为热点。前几名的变化不需要实时反映给每个用户,可以在应用的本地缓存中保存 1 秒,大部分请求不再访问 Redis,见 [多级缓存](缓存.md#多级缓存)。 + ### 其它 Set 可以实现交集、并集等操作,从而实现共同好友等功能。 @@ -450,6 +456,8 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 将某个时间点的所有数据都存放到硬盘上。 +执行 BGSAVE 或满足 save 配置的条件时,Redis fork 一个子进程把内存中的数据写入 RDB 文件,主进程继续处理命令。fork 之后父子进程共享同一份内存页,主进程修改某一页时操作系统才复制这一页(写时复制),所以生成快照期间写入越多,额外占用的内存越多。 + 可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。 如果系统发生故障,将会丢失最后一次创建快照之后的数据。 @@ -474,6 +482,12 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 +重写同样由子进程完成,它不读取旧的 AOF 文件,而是按内存中的当前数据生成最少的命令。Redis 7.0 起 AOF 由多个文件组成:重写生成的基础文件,以及之后追加写命令的增量文件。 + +

+ +配置项 aof-use-rdb-preamble 默认开启,重写生成的基础文件使用 RDB 格式,称为混合持久化:重启时先载入 RDB 部分,再重放增量文件中的命令,兼顾了 RDB 恢复快和 AOF 丢失数据少的优点。两种持久化都开启时,重启后使用 AOF 恢复,因为它的数据通常更完整。 + ## 九、事务 一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 @@ -482,6 +496,54 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 +Redis 的事务不支持回滚:EXEC 时某条命令执行出错,其它命令照常执行。例如 name 的值是字符串 alice,对它执行 INCR 会出错,但同一事务中的两条 SET 都生效: + +```text +> multi +OK +> set city hangzhou +QUEUED +> incr name +QUEUED +> set age 25 +QUEUED +> exec +1) OK +2) (error) ERR value is not an integer or out of range +3) OK +``` + +命令入队时就能发现的错误(例如命令不存在),EXEC 会放弃整个事务,返回 EXECABORT。 + +WATCH 可以实现乐观锁:EXEC 之前被 WATCH 的键被其它客户端修改,EXEC 返回 nil,事务中的命令都不执行,由客户端决定是否重试。 + +### Lua 脚本 + +事务中的命令在 EXEC 时才一起执行,不能根据前一条命令的结果决定下一条做什么。Lua 脚本可以做到这一点,并且整个脚本原子执行,执行期间不会插入其它客户端的命令。 + +例如每个用户每天最多领 3 张券,键为 coupon:1001,有效期一天: + +```lua +local n = tonumber(redis.call('GET', KEYS[1]) or '0') +if n >= tonumber(ARGV[1]) then + return 0 +end +redis.call('INCR', KEYS[1]) +if n == 0 then + redis.call('EXPIRE', KEYS[1], ARGV[2]) +end +return n + 1 +``` + +用 redis-cli --eval coupon.lua coupon:1001 , 3 86400 连续执行 4 次,返回值依次为 1、2、3、0。如果在客户端先 GET 再 INCR,两个请求可能同时读到 2,都领到第 3 张。 + +需要注意的是: + +- 脚本中途出错时,之前执行的写命令不会回滚,之后的命令不再执行; +- 脚本执行期间服务器不处理其它命令,脚本要短,不要在其中遍历大集合; +- 脚本用到的键要通过 KEYS 传入,集群模式下这些键必须在同一个槽中; +- 可以先用 SCRIPT LOAD 把脚本缓存在服务器上,之后用 EVALSHA 加脚本的 SHA1 调用,不用每次发送脚本内容。 + ## 十、事件 Redis 服务器是一个事件驱动程序。 @@ -492,7 +554,7 @@ Redis 服务器是一个事件驱动程序。 Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。 -

+

### 时间事件 @@ -525,7 +587,7 @@ def aeProcessEvents(): # 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定 aeApiPoll(timeval) # 处理所有已产生的文件事件 - procesFileEvents() + processFileEvents() # 处理所有已到达的时间事件 processTimeEvents() ``` @@ -545,7 +607,20 @@ def main(): 从事件处理的角度来看,服务器运行流程如下: -

+

+ +### 单线程为什么快 + +Redis 用一个线程执行所有命令,仍然很快,原因主要有以下几点: + +- 数据在内存中,一条 GET 只需要查一次哈希表; +- 单线程执行命令,不需要加锁,也没有线程切换的开销; +- I/O 多路复用让一个线程同时处理大量连接; +- 按数据的大小选择不同的编码,例如元素少时用紧凑的 listpack。 + +在本机(Apple M1 Pro,Redis 7.4)用 redis-benchmark 测试,50 个连接逐条发送 GET,每秒约 13 万次;每次连续发送 16 条命令(流水线)时,每秒约 140 万次。逐条发送 100 万次 GET 时,服务器消耗的 CPU 时间中内核态 6.35 秒、用户态 1.18 秒:大部分时间花在网络读写的系统调用上,解析和执行一条命令只需要 1 微秒左右。 + +所以 Redis 的瓶颈通常是网络和内存,而不是 CPU。Redis 6.0 加入了 I/O 多线程(io-threads,默认关闭),只用多个线程读写套接字,命令仍由主线程逐个执行,单条命令的原子性不变。此外,关闭文件、AOF 刷盘和 UNLINK 释放大键的内存这些耗时操作由后台线程完成。 ## 十一、复制 @@ -561,16 +636,34 @@ def main(): 3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。 +Redis 7.0 起默认不把快照写入磁盘,而是直接通过网络发送给从服务器(repl-diskless-sync yes)。 + +以上是第一次连接时的全量复制。从服务器断线重连后,会带上复制 ID 和自己已经复制到的偏移量发送 PSYNC 命令:如果主服务器的复制积压缓冲区(默认 1 MB)中还保存着之后的写命令,就只发送缺少的部分,称为部分重同步;否则重新进行全量复制。写入量大、断线时间长时,可以调大 repl-backlog-size,避免频繁全量复制。 + ### 主从链 随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。 -

+

## 十二、Sentinel Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 +通常部署 3 个或以上的哨兵,故障转移的过程如下: + +1. 主观下线:某个哨兵在 down-after-milliseconds 内没有收到主服务器的有效回复,认为它下线; +2. 客观下线:认为主服务器下线的哨兵数达到配置的 quorum,确认它下线; +3. 选出领头哨兵:哨兵之间投票,得到多数哨兵投票的一个负责这次故障转移,避免多个哨兵同时操作; +4. 选择新的主服务器:排除断线太久的从服务器,再依次比较优先级(replica-priority,越小越优先)、复制偏移量(数据越新越好)和运行 ID; +5. 切换:让选中的从服务器执行 REPLICAOF NO ONE,其它从服务器改为复制它,客户端从哨兵查询到新的地址;原来的主服务器恢复后成为从服务器。 + +

+ +实验中另一个从服务器 6401 改为复制 6402 时,6402 的日志为 Partial resynchronization request from 127.0.0.1:6401 accepted,只发送了 156 字节的积压数据;而重启的 6400 复制 ID 不匹配,只能全量复制。 + +需要注意的是,主从之间是异步复制,切换时还没有复制到从服务器的写入会丢失。网络分区时,旧的主服务器还可能继续接受写入(脑裂),恢复后它变为从服务器,这段时间的写入也会丢失。可以配置 min-replicas-to-write 1 和 min-replicas-max-lag 10:延迟不超过 10 秒的从服务器少于 1 个时,主服务器拒绝写入并返回 NOREPLICAS,把丢失的范围限制在 10 秒左右。 + ## 十三、分片 分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。 @@ -616,9 +709,9 @@ Redis Cluster 把键空间分为 16384 个哈希槽,每个主节点负责一 文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。 -Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。 +Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:1001,其中 article 为命名空间,ID 为 1001。 -

+

### 点赞功能 @@ -626,22 +719,27 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据 为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。 -

+

### 对文章进行排序 -为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的) +为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。两个有序集合的成员都是文章 ID,分值分别是发布时间和点赞数,按分值倒序取前 10 个就是首页要显示的文章。 -

+

## 参考资料 - Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013. - [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html) -- [REDIS IN ACTION](https://redislabs.com/ebook/foreword/) +- [REDIS IN ACTION](https://web.archive.org/web/20210421115011/https://redislabs.com/ebook/foreword/) - [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/) - [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html) - [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide) - [Redis 应用场景](http://www.scienjus.com/redis-use-case/) - [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) - [Redis: Scale with Redis Cluster](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/) +- [High availability with Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/) +- [Redis replication](https://redis.io/docs/latest/operate/oss_and_stack/management/replication/) +- [Redis persistence](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/) +- [Transactions | Redis](https://redis.io/docs/latest/develop/interact/transactions/) +- [Scripting with Lua | Redis](https://redis.io/docs/latest/develop/interact/programmability/eval-intro/) diff --git a/notes/pics/redis-aof-rewrite.png b/notes/pics/redis-aof-rewrite.png new file mode 100644 index 00000000..7eb917a9 Binary files /dev/null and b/notes/pics/redis-aof-rewrite.png differ diff --git a/notes/pics/redis-article-hash.png b/notes/pics/redis-article-hash.png new file mode 100644 index 00000000..3cf4e5a8 Binary files /dev/null and b/notes/pics/redis-article-hash.png differ diff --git a/notes/pics/redis-article-vote.png b/notes/pics/redis-article-vote.png new file mode 100644 index 00000000..3bc27d25 Binary files /dev/null and b/notes/pics/redis-article-vote.png differ diff --git a/notes/pics/redis-article-zsets.png b/notes/pics/redis-article-zsets.png new file mode 100644 index 00000000..7b5f5539 Binary files /dev/null and b/notes/pics/redis-article-zsets.png differ diff --git a/notes/pics/redis-event-loop.png b/notes/pics/redis-event-loop.png new file mode 100644 index 00000000..fb9e789c Binary files /dev/null and b/notes/pics/redis-event-loop.png differ diff --git a/notes/pics/redis-file-event.png b/notes/pics/redis-file-event.png new file mode 100644 index 00000000..50091b87 Binary files /dev/null and b/notes/pics/redis-file-event.png differ diff --git a/notes/pics/redis-hash.png b/notes/pics/redis-hash.png new file mode 100644 index 00000000..2d4f7deb Binary files /dev/null and b/notes/pics/redis-hash.png differ diff --git a/notes/pics/redis-list.png b/notes/pics/redis-list.png new file mode 100644 index 00000000..76e313fa Binary files /dev/null and b/notes/pics/redis-list.png differ diff --git a/notes/pics/redis-replica-chain.png b/notes/pics/redis-replica-chain.png new file mode 100644 index 00000000..2a836574 Binary files /dev/null and b/notes/pics/redis-replica-chain.png differ diff --git a/notes/pics/redis-sentinel-failover.png b/notes/pics/redis-sentinel-failover.png new file mode 100644 index 00000000..d634440e Binary files /dev/null and b/notes/pics/redis-sentinel-failover.png differ diff --git a/notes/pics/redis-set.png b/notes/pics/redis-set.png new file mode 100644 index 00000000..fa68c55c Binary files /dev/null and b/notes/pics/redis-set.png differ diff --git a/notes/pics/redis-skiplist.png b/notes/pics/redis-skiplist.png new file mode 100644 index 00000000..d02bb99f Binary files /dev/null and b/notes/pics/redis-skiplist.png differ diff --git a/notes/pics/redis-string.png b/notes/pics/redis-string.png new file mode 100644 index 00000000..9260ae15 Binary files /dev/null and b/notes/pics/redis-string.png differ diff --git a/notes/pics/redis-zset.png b/notes/pics/redis-zset.png new file mode 100644 index 00000000..e10c26f5 Binary files /dev/null and b/notes/pics/redis-zset.png differ