为什么选择使用 Redis
速度快
- 内存
- C 语言
- 单线程,预防多线程竞争
基于键值对的数据结构
- 五种基础数据结构,衍生多种数据结构 (Bitmaps、HyperLogLog、GEO)
丰富的功能
- Key 过期,实现缓存
- 发布订阅,实现消息系统
- 支持 Lua 脚本,可以创造新的 Redis 命令
- 支持简单的事务
- 提供流水线 (Pipeline),一批命令一次性传到 Redis,减少网络开销
简单稳定
- 代码少
- 单线程,模型简单
- 不依赖操作系统中的类库,自己实现事件处理相关功能
客户端语言多
- Java
- PHP
- Python
- C
- C++
- Nodejs
- 等等...
持久化
- RDB
- AOF
主从复制
高可用和分布式
- Redis Sentinel (Reids 2.8)
- Redis Cluster 提供真正分布式、高可用、读写和容量扩展
为什么单线程还能这么快
- 纯内存访问:内存响应时间大概为 100 纳秒,这是每秒达到厅万级重要基础
- 非阻塞 I/O:epoll I/O 多路复用技术,自身事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 浪费时间
- 单线程避免线程切换和竞态产生的消耗
单线程的优点:
- 可以简化数据结构和算法的实现
- 单线程避免多线程切换和竞态产生的消耗,锁和线程切换通常是性能杀手
单线程缺点:
- 每个命令不能执行过长,会造成其他命令阻塞
使用场景
String 使用场景
- 缓存 (string) {MySQL 缓存}
- 计数器 (string[incr])
- 共享session {负载均衡的 web 服务器 session 集中管理}
- 限速 {手机验证码 1 分钟不能超过 5 次,一个 IP 地址不能在 1 秒访问 n 次} set ex nx
Hash 使用场景
- 关系型数据库列属性
id | name | age | city |
---|---|---|---|
1 | tom | 23 | beiing |
2 | mike | 30 | tianjing |
user:1 ->
id | name | age | city |
---|---|---|---|
1 | tom | 23 | beijing |
user:2
id | name | age | city |
---|---|---|---|
2 | mike | 30 | tianjing |
其实有三种方法缓存用户信息
1. 原生字符串:每个属于一个 Key
set user:1 name tom
set user:1 age 23
set user:1 city beijing
优点:简单直观,每个属性都支持更新操作
缺点:占用过多 key,内存占用大,用户信息内聚性差,一般不会在生产环境使用
2. 序列化字符串:将用户信息序列化用一个 key 保存
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存使用率
缺点:序列化、反序列化有性能开销,每次更新属性需要把所有数据全取出来反序列化,更新后,序列化到 Redis
3. Hash:每个用户属性使用一对 "filed - value",但只用一个 key 保存
hset user:1 name tom age 23 city beijing
优点:简单直观,如果使用合理可以减少内在空间使用
缺点:要控制 ziplist、hashtable 两种内部编码的转换,hashtable 会消耗更多内存
List 使用场景
消息队列
lpush + brpop 组合实现阻塞队列,生产者使用 lpush 从左侧插入元素,多个消费使用 brpop 阻塞式"抢"列表尾部元素,多个客户端保证消费的负载均衡和高可用
文章列表
List 有序、支持按照索引取元素
# 每个用户自己的文章列表 用户(key) --> 文章(list) --> 文章内容(hash) 每篇文章使用 Hash,每篇文件有 3 个属性 title、timestamp、content hmset article:1 title xx timestamp 1476536196 content xxxx ... hmset article:k title xx timestamp 1476512536 content yyyy ... 向用户文章列表添加文章 user:{id}:acticles 作为文件列表的 Key lpush user:1:article article:1 article:3 ... lpush user:k:article article:5 分页获取用户文章列表,下面伪代码将用户 id=1 的前 10 篇文件 articles = lrange user:1:articles 0 9 for article in {articles} hgetall {article}
注:使用 List 存文章两个问题
- 每次分页获取的文章个数较多,需要执行 hgetall 多次,可以考虑使用 pipline批量获取,或者考虑文章数据序列化为字符串类型,使用 mget 批量获取。
- 分页获取文章列表时,lrange 命令在列表两端性能较好,但如果列表较大,获取列表中间范围元素性能会变差,可以考虑将 List 做二级分拆或使用 Redis 3.2 的 quicklist 内部编码实现,它结果 ziplist、linkedlist 特点,获致列表中间范围的元素也可以高效完成
栈 lpush + lpop = Stack
队列 lpush + rpop = Queue
有限集合 lpush + ltrim = Capped Collection
消息队列 lpuush + brpop = Message Queue
SET 使用场景
- 标签 tag
# 给用户添加标签
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:2:tags tag1 tag2 tag4
...
# 给标签添加用户
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2
...
# 删除用户下的标签
srem user:1:tags tag1 tag5
...
# 删除标签 下的用户
srem tag1:users user:1
srem tag5:users user:1
# 计算用户共同感兴趣的标签
sinter user:1:tags user:2:tags
用户和标签的关系维护应该在一个事务内执行,防止部分命令失败造成数据不一致,如何来实现,要依赖 Lua
- 生成随机数,抽奖 spop/srandmember = Random item
- sadd + sinter = Social Graph 社交需求
ZSET 使用场景
- 排行榜
# 添加用户赞数,用户 mike 上传一个视频,并获取 3 个赞
zadd user:ranking:2016_03_15 3 mike
如果再获取赞
zincrby user:ranking:2016_03_15 1 mike
# 取消用户赞
zrem user:ranking:2016_03_15 mike
# 展示获取赞数最多的十个用户
zrevrangebyrank user:ranking:2016_03_15 0 9
# 展示用户信息以及用户分数
将用户名作为 Key 后缀,将用户信息保存在哈希类型中,用户的分数和排名可使用 zscore、zrank
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike