Why Redis

2017/7/14 浏览量: - | posted in  Redis comments

为什么选择使用 Redis

  • 速度快

    • 内存
    • C 语言
    • 单线程,预防多线程竞争
  • 基于键值对的数据结构

    • 五种基础数据结构,衍生多种数据结构 (Bitmaps、HyperLogLog、GEO)
  • 丰富的功能

    • Key 过期,实现缓存
    • 发布订阅,实现消息系统
    • 支持 Lua 脚本,可以创造新的 Redis 命令
    • 支持简单的事务
    • 提供流水线 (Pipeline),一批命令一次性传到 Redis,减少网络开销
  • 简单稳定

    • 代码少
    • 单线程,模型简单
    • 不依赖操作系统中的类库,自己实现事件处理相关功能
  • 客户端语言多

  • 持久化

    • 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 存文章两个问题

    1. 每次分页获取的文章个数较多,需要执行 hgetall 多次,可以考虑使用 pipline批量获取,或者考虑文章数据序列化为字符串类型,使用 mget 批量获取。
    2. 分页获取文章列表时,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