简介
Redis 的全称是 Remote Dictionary Server,是一个使用 C 语言编写的、开源的(BSD 许可)高性能非关系型(NoSQL)的键值对数据库。
Redis 的数据是存储在内存中的,所以读写速度非常快,被广泛应用于缓存方向,当然也有持久化数据库的用法。
优缺点
优点
- 读写性能优异, Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s
- 数据类型丰富,有 String、List、Hash、Set、SortedSet 等
- 单线程原子性,Redis 所有的操作都是原子性的,也支持多个操作合并后的原子执行
- 丰富的特性,Redis 支持发布订阅、通知、key 过期等功能
- 支持持久化,Redis 支持 RDB、AOF 等持久化方式
- 高可用性,Redis 支持主从复制、哨兵模式、Cluster 等高可用方式
缺点
- 数据库容量受物理内存的限制,不能用作海量数据的读写
- Redis 难以支持在线扩容,修改配置文件之后重启 Redis,恢复磁盘上的数据耗费时间较久
使用场景
数据缓存
Redis 是高性能的内存数据库,因此,缓存是 Redis 最常用的场景。Redis 作为缓存使用的时候,一般是采用先更新数据库,再删除缓存的 Cache Aside Pattern 策略。
整体的逻辑是:业务从 Redis 中读取数据,如果 Redis 中访问不到数据,然后读取数据库中的数据,将数据库中的数据缓存到 Redis 中;业务更新数据,直接修改数据库中的数据,然后将 Redis 中的缓存删除。
这种方案需要注意的就是:避免缓存击穿,数据的实时性相对较低。
限时业务
Redis 支持给 key 设置过期时间,客户端无法访问到过期的 key,利用这一特性可以运用在限时的优惠活动、手机验证码等业务场景。
计数器
Redis 的 INCRBY
命令可以实现原子性的递增,可以直接作为计数器存储和递增。尤其是,该命令在键不存在时会直接初始化值再执行 INCRBY
命令,执行成功后会直接返回计算增量之后的数值。
高并发的秒杀活动、分布式序列号的生成、限制手机发送短信次数、接口限制访问次数等需要计数的功能,都涉及到计数器的概念,尤其是分布式系统会涉及到分布式计数器。
分布式锁
先使用 SETNX
命令来争抢锁,结果返回 1
表示设置成功,抢到之后再用 EXPIRE
命令给锁加一个过期时间防止忘记释放锁。
从 Redis 的 2.6.12 版本开始,可以通过 SET
命令的复杂参数,将 SETNX
命令和 EXPIRE
命令合并成一条命令来使用:
-
EX second
: 设置键的过期时间为 second 秒,使用EX
选项效果等同于SETEX
命令。 -
PX millisecond
: 设置键的过期时间为 millisecond 毫秒,使用PX
选项效果等同于PSETEX
命令。 -
NX
: 只在键不存在时,才对键进行设置操作,使用NX
选项效果等同于SETNX
命令。 -
XX
: 只在键已经存在时,才对键进行设置操作。
同样的,使用 SET
命令操作成功之后会返回 OK
,这样才表示抢到了锁。
为了避免分布式锁被误删,加锁时可以设置线程 ID 作为 value 值,删除时需要线程的线程 ID 和 Redis 存储的值一致才能够删除分布式锁,否则只能等待锁自动过期,整个删除过程使用事务的方式保证原子性。
排行榜功能
通过 Redis 的 SortedSet 可以实现排行榜功能。
比如说需要展示点赞排行榜,可以将用户 ID 作为 SortedSet 的 value 值,将用户的点赞数作为 SortedSet 的 score 值,SortedSet 提供的 ZRANGEBYSCORE
命令可以快速返回已排序的点赞排行榜。
延时队列
延时队列其实就是一个带有延迟功能的消息队列,Redis 可以通过 SortedSet 实现延时队列。
具体的实现如下:
- 将消息内容序列化成一个字符串作为 SortedSet 的 value 值,这个消息的到期处理时间作为 score,生产者调用
ZADD
命令生产消息,消费者可以使用ZRANGEBYSCORE
命令获取一段时间之前的数据轮询处理; - 通常使用多个线程轮询 SortedSet 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其他线程可以继续处理;
- 因为有多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行,Redis 的
ZREM
命令是多线程争抢任务的关键,ZREM
命令会返回被成功移除的成员数量,可以通过ZREM
命令来决定任务的唯一属主; - 同时也要注意一定要进行异常捕获,避免因为个别任务处理问题导致循环异常退出,同一个任务可能会被多个进程取到之后再使用
ZREM
命令进行争抢,那些没抢到的进程都是白取了一次任务,这是浪费; - 通常可以使用 Lua 脚本的方式,将
ZRANGEBYSCORE
命令和ZREM
命令一同挪到服务器端进行原子化操作,这样多个进程之间争抢任务时就不会出现这种浪费了。
异步队列
第一种方案 是使用 List 结构作为异步队列,RPUSH
命令生产消息,LPOP
命令消费消息。当使用 LPOP
命令没有得到消息的时候,需要适当 sleep 一会再重试,在这里 sleep 会导致消息的处理延迟增加。
如果不做 sleep 重试,改进的 第二种方案 是,使用 LPOP
命令的阻塞版本 BLPOP
命令,在 List 队列中没有消息的时候,它会阻塞直到消息到来,一旦数据到来,则立刻醒过来,消息的延迟几乎为零。但是如果线程一直阻塞,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候 BLPOP
命令会抛出异常来,代码中需要处理这样的异常。
除了 List 队列之外,第三种方案 是使用 Pub/Sub 订阅者模式实现一对多的消息队列。但是 Redis 的发布订阅功能是无状态的,对于发布者来说,无法知道发布的消息是否被订阅者接收到,在消费者下线的情况下,生产的消息会丢失。