Redis 特性:
- 速度快,数据在内存中,通过 key 查找,时间复杂度 O(1)
- 支持多种数据类型,string,list,hash,set,sort set 等
- 支持事物,操作都是原子性的
- 丰富的特性,可用于缓存等
Redis 是单线程还多线程?
答:Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO 多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
参考和图片链接:https://www.jianshu.com/p/6264fa82ac33
1、Redis 基础
1.1 为什么要使用 Redis?
当系统访问量大的时候,比如某个商品促销导致订单量激增(比如秒杀场景),如果直接在 MySQL 扣减库存很容易导致 MySQL 崩掉,所以需要一个 Redis 这样的缓存中间件。
1.2 常用的缓存中间件有那些?
知道的有 Redis 和 Memcache
- 共同点:都是内存数据库,
- 不同点:Redis 支持持久化写入到磁盘,而 Mencache 挂掉后就消失无法恢复。
1.3 Redis 有那些数据结构
Redis 的基本数据结构如下:
图片和参考来源:https://www.cnblogs.com/haoprogrammer/p/11065461.html
- String :存储字符串,比较浪费内存,不推荐。可以用来 Session 共享,分布式锁等;
- Hash:比 String 省内存,可以比较直观的缓存多维信息,而 String 需要通过 JSON 等形式存储多维信息
- List:链表,异步队列需要延后处理的任务塞进列表,存储微博、朋友圈的时间轴列表
- Set:去重,点赞,还有收藏等
- Sort Set:去重,有个 score,可以用来排名等
1.4、Redis 数据过期时间过于集中会导致那些问题?
会导致卡顿,解决方法在设置过期时间的时候加一个随机值。使得过期时间分散一点。消息过期太集中容易导致缓存雪崩。
1.5 Redis 分布式锁,需要主要什么?
更多参考 Spring Boot 和 Redis 的分布式锁: https://ylooq.gitee.io/learn-spring-boot-2/#/12-DistributedLock
同一商品的信息、促销优惠等,可能会被多个人同时更改,如果没有加分布式锁会导致数据前后不一致的问题。
# 设置 锁的命令,正确示范
SET key value [EX seconds] [PX milliseconds] [NX|XX]
SET 参数说明,从 Redis 2.6.12 版本开始:
- 没有 EX、NX、PX、XX 的情况下,如果 key 已经存在,则覆盖 value 值不管其什么类型;如果不存在,则新建一个 key -- value
EX seconds
: 设置过期时间为 seconds 秒,SET key value EX second
效果等同于SETEX key second value
PX millisecond
:设置键的过期时间为millisecond
毫秒。SET key value PX millisecond
效果等同于PSETEX key millisecond value
。NX
:只在 key 不存在时,才对键进行设置操作。SET key value NX
效果等同于SETNX key value
。XX
:只在 key 已经存在时,才对键进行设置操作。- value 值:需要加入随机的字符串,作为释放锁的唯一口令(Token),预防持有过期锁的线程误删其他持线程的锁
需要注意:设置锁的时候不能先设置 key,然后在设置过期时间,如果执行两个命令之间 Redis 进程发生意外(crash,重启维护)了,就会导致锁无法自动释放
# 设置 key value ,这是错误的
SET key value
# 然后再设置过期时间 10s,万一中间服务器抽风了,那就没法自动释放锁,容易导致死锁了
EXPIRE key 10
如果在代码,参考如下设置设置,同时设置 key vlaue 和过期时间:
# 加锁成功,lockStat !=null && lockStat == true
Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8),
Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
需要注意:释放锁的时候不能直接 DEL,要使用 lua 脚本验证 key 和 value,一致才能释放锁,可能导致先前持有过期锁的线程误删除锁。
if redis.call('get', KEYS[1]) == ARGV[1]
then return redis.call('del', KEYS[1])
else return 0 end
需要注意:加锁失败的处理
- 抛出异常
- sleep,然后再次尝试
- 使用延时队列
1.6 Redis 的 keys 命令和 sacn 命令有什么区别?
keys abc*
: keys 可以匹配后面的正则表达式,例如列出所有 abc 开头的 keyscan 0【游标】 MATCH abc* count 1【返回多少条】
:列出所有 abc 开头的 key
参考 : http://doc.redisfans.com/key/scan.html
keys 会阻塞,一直到所有符合条件的 key 列出来,如果key 的总数或者符合条件的 key 过多会导致卡顿,而 SCAN 是增量式迭代命令,每次从游标开始,游标参数被设置为 0
时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0
的游标时, 表示迭代已结束。
不过,增量式迭代命令也不是没有缺点的:举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。
Redis> keys key* # 测试数据
1)key1
*********
10000)key10000
redis>scan 0 match key* count 100
# 0 表示开始迭代的游标,match key* 正则表达式匹配,count 100 返回 100 条
1)120 #---> 下一次使用的游标
2)1) "key233" #--->返回的结果,不一定按照 key1 到 key10000 的顺序
2) "key1000"
3) "key330"
4) "key23"
1、如果 redis 中有 1 亿个 key,其中有 10w 个 abc 开头的 key,怎么列出来? 用 keys 命令
2、如果 redis 正在提供服务,keys 会导致什么后果?卡顿,可以通过 sacn 命令解决,但 scan 命令返回的结果不能完全保证,因为在增量迭代的时候 key 可能会被修改。
1.7 Redis 如何实现消息队列?
在 Redis 中,有个 list 的数据结构,通过如下命令生产消息和消费消息,List 是双链表,遵循先入先出的原则,通过 lpush/rpush 和 rpop/lpop 发布和消费消息。
# 消息入队,L 左边,R 右边,PUSH 入队,POP 出队
redis> LPUSH languages python
redis> LPUSH languages java
# 消息出队,返回 python ,如果队列里面为空,则返回 nil
redis> RPOP languages
只能被 1 个消费端消费,LPOP 命令在队列为空的时候返回 null,如果一直没有消息,可以让消费者线程睡睡觉 sleep 免得浪费资源。
如果消费者线程不使用 sleep() ,该怎么办?使用 BLPOP / 或者 BRPOP 命令,会阻塞到消息队列有消息才返回。
1.8 Redis 如何实现发布、订阅模式?也就是 1:N 消费?
使用 Redis 的 pub/sub 发布定义模式,但不推荐使用,因为消息者下线的情况下,消息会丢失。推荐使用专门的消息中间件如 RocketMQ,RabbitMQ,Kafka。
订阅某个 key 的消息:http://doc.redisfans.com/pub_sub/subscribe.html
# 订阅 msg 频道
# 1 - 3 行是执行 subscribe 之后的反馈信息
# 第 4 - 6 行才是接收到的第一条信息
redis> subscribe msg
Reading messages... (press Ctrl-C to quit)
1) "subscribe" # 返回值的类型:显示订阅成功
2) "msg" # 订阅的频道名字
3) (integer) 1 # 目前已订阅的频道数量
1) "message" # 返回值的类型:信息
2) "msg" # 来源(从那个频道发送过来)
3) "hello moto" # 信息内容
发布某个 key 的消息:http://doc.redisfans.com/pub_sub/publish.html
redis> publish msg "good morning"
(integer) 1 # 返回当前 msg 主题的消息订阅客户端数量
1.9 Redis 如何实现延迟队列?
如果是多线程环境处理延迟队列,可以通过 zrangebyscore 和 zrem 一同挪到服务器端使用 lua 脚本进行原子操作。
- zrangebyscore 只取一条数据
- zrem 移除该条数据
使用 Sorted Set 数据类型,使用时间戳作为分数
# 添加队列任务
redis> ZADD key score(时间戳) value
获取 N 秒前的数据
# 使用 ZRANGEBYSORT 获取前 N 秒的数据 ,0,1 类似 MySQL limit,偏移量 0,取一条
redis> ZRANGEBYSORT key (时间戳开始点 时间戳结束点 0,1
# score 开始点, score 结束点, 也就是列出 时间戳开始点 <= 时间戳 < 时间戳结束点 的数据
# 如果开始点或者结束点前面有 ( 这个符号表示 大于等于或者小于等于
1.10 Redis 持久化
前面提到,Redis 支持持久化的操作,Redis 的数据全部在内存中,如果突然挂机,如果不持久化写入磁盘就会造成数据的全部丢失。Redis 的持久化机制有两种:一种是 RDB 快照,一种是 AOF 日志。
通常情况下,Redis 主节点不进行持久化操作,持久化操作一般在从节点进行的。
1.10.1 RDB 快照原理:全量持久化
RDB 会将内存的数据全量的写入到磁盘,这也是为什么叫“快照”的原因。Redis 使用多进程的 COW(Copy On Write)实现内存数据的快照持久化。数据恢复比较快。
RDB 的缺点的:会丢失很多的数据。看 COW 原理理解。
什么是 COW? 参考 Copy On Write机制了解一下 https://cloud.tencent.com/developer/article/1369027
Redis 在持久化的时候,会将当前主进程调用 glibc 的 fork 产生一个子进程。fork 出来的子进程和父进程共享相同的内存页,只有在数据更改的时候才会分离给子进程分配新的物理内存。在 Redis 中,子进程不会对内存中的数据进行修改。当父进程对其中一个 page 修改的时候,会使用操作系统的 COW 机制,将内存分离出来。
父子进程虚拟地址不同,如果内存页数据在 fork 进程后没有发生改变,则实际物理内存地址相同。
Redis 主进程对红色的 page 进行修改,COW 会将父子进程共享的红色 page 在修改前分离。子进程还是会使用未修改的内存页。这也就是说,Redis 的内存在子进程产生的一瞬间瞬间凝固了,相当于给内存照相(快照)了。
因为在 Redis 中,父进程(主进程)负责响应请求,也就是会修改内存中的数据。而子进程专门负责遍历内存,进行系列化写入磁盘。
主进程修改内存页,会拷贝未修改的内存页给子进程分配新的物理内存。
1.10.2 AOF 日志
AOF 日志,也就是记录对 Redis 内存数据进行修改的指令,当 Redis 宕机重启后,会根据日志记录重放一遍(重新执行一遍)。这样子就可以恢复到宕机前的数据了。
Redis 收到客户端修改指令后,进行参数检验,逻辑处理,如果没有执行成功,就会记录一条数据。这跟其他的 HBASE 等不同的是,Redis 是先执行再记录日志的。
AOF 日志的缺点是:如果 AOF 日志很大很大重新执行一遍 AOF 日志很慢很慢。
AOF 瘦身: 原理就是 fork 一个子进程,对当前的内存数据遍历,生成新的 AOF 日志,然后跟 fork 后的增量数据合并产生的 AOF。因为 Redis 经常 set 、del,有些已经删除了的数据没必要还记录着。
AOF 会丢失数据吗? 其实 AOF 为了快速写入,还是会先写入到内存中,所以没有 fsync 刷写到磁盘的数据还是会丢失的。不调用 fsync ,交给操作系统完成写入磁盘的操作性能比较高,但数据丢失的更多。一条日志调用一次 fync,则性能比较低。在生产中一般配置 1s fsync 刷写磁盘,也就是说最多丢失 1s 的数据。
Kafka 默认是将刷写磁盘的操作交给操作系统的。所以 Kafka 比较适合允许丢失少量数据的大数据应用。
1.10.3 Redis 4 开始的混合持久化
Redis 重启后,会加载 RDB,然后在重放 AOF,这样子数据丢失的可能比较小,重启的效率也比较高。
1.11 Redis Pipeline 管道
管道是加速 Redis 的存取效率,减少网络 IO 次数,减少 IO 时间。也就是将多个读取、写入的操作指令封装成一个网络请求。
Kafka 客户端,将发往同一个 Broker 的消息封装成 batchs,然后再发送,提高吞吐量。
如何测试 Redis 的 QPS,redis-benchmark:
> redis-benchmark -t set -p 3 -q
# -t set 对 set 命令进行压测
# -p 3 管道的命令数量,不使用表示 一个命令一个请求
# -q 强制退出 redis。仅显示 query/sec 值
1.12 Redis 同步机制
Redis 支持主从同步和从从同步:
1.12.1 快照同步
第一次同步或者新节点加入的时候使用快照同步。首先在主节点进行 bgsave 产生 RDB 快照,然后发送给要同步的从节点。
从节点接收 RDB 后会清空当前从节点的数据,全量加载 RDB 快照。对于快照以后的数据采用增量同步的方式。
1.12.2 增量同步
Redis 增量同步的是指令流,主节点会将会改变内存数据的指令记录到本地的环形数组 buffer,然后异步将 buffer 发送到从节点,从节点一边记录同步到那里一边反馈给主节点同步到哪里了(偏移量)。
环形数组 buffer,循环写入,写满后会覆盖前面写的内容。如果网络不好,从节点短时间无法和主节点同步。恢复网络后,主节点环形 buffer 那些还没有被同步的数据可能被后续的指令覆盖了。那么就会采用快照同步的方式。
快照同步死循环:主节点 bgsave 后的增量数据的操作指令流,又发生覆盖了前面还没有同步的指令。从节点又得重新采用快照同步方式。然后死循环了。因此需要配置一个合适的 buffer 大小。
1.13 Redis 集群高可用
Redis Sentinel :哨兵,master 宕机后,将 slave 提升为 master 继续提供服务。数据丢失:因为 buffer 增量同步的方式是异步的,可能会丢失部分数据。高可用
Redis Cluster:去中心化,多个节点负责集群的一部分数据,多个节点也是对等的。默认分成 16384 个槽位,每个节点负责部分数据。扩展性。
参考资料:
Redis基础:https://mp.weixin.qq.com/s/aOiadiWG2nNaZowmoDQPMQ
钱文品《Redis 深度历险》