KV存储之Redis完全指北

Redis

 Redis(Remote Dictionary Server)是一个开源(BSD协议)的高性能 Key-Value 型内存数据库,提供多种语言的客户端API。Redis 也被称为数据结构服务器,它存储的value 可以是字符串、哈希、列表、集合和有序集合等常用的数据类型。

发展历程

版本 时间线 描述
Redis 3.0 2015年4月 Redis Cluster: Redis的官方分布式实现
Redis 3.2 2016年5月 新的List编码类型:quicklist
Redis 4.0 2017年7月 提供了新的缓存剔除算法:LFU
Redis 5.0 2018年10月 新的Stream 数据类型
Redis 6.0 2020年4月 多线程 IO
Redis 7.0 2022年4月

下载和安装

  • 在生产环境或者开发环境,都可以用docker的方法来安装redis。
  • 在开发环境中,windows下最简单的办法是安装针对windows平台编译的redis版本,可以在 这里 下载合适的新版本,建议安装6.0以上的版本。

功能特性

  • 高性能 (单机读 11W次/s,写8W次/s)
  • 高可用 (主从、哨兵、集群)
  • 支持多种数据类型(字符串、hash、列表、集合、有序集合)
  • 功能丰富:支持数据持久化、事务、发布订阅、Lua脚本等功能
  • 单线程工作模式,并发安全;采用IO多路复用机制
  • Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。

客户端

命令行

​ Redis 支持客户端使用CLI 命令行的方式与服务器进行交互,所有命令在官网都能查到详细说明,本文只示例几种常用的操作,更全面的命令操作请参考官网。

命令 描述
DEL key 删除key,返回删除的条数
EXISTS key 检查 key 是否存在,存在返回1,不存在返回0
RENAME key newkey 重命名key
RENAMENX key newkey 仅当 newkey 不存在时, 重命名key
TYPE key 返回key所存储的value 类型
MOVE key db 将key 移动到其他数据库
TTL key 查看key的过期时间,单位:秒;-1代表永不过期,-2表示已经过期;
EXPIRE key [seconds] 设置key的过期时间,单位:秒;
EXPIREAT key [timestamp] 设置key的过期时间,同上不同的是设置UNIX 时间戳;
PERSIST key 移除key的过期时间, key永不过期;
KEYS [pattern] 查找符合给定表达式的所有key

Java 客户端

​ Redis支持几十种编程语言的客户端操作,如 C、C++、C#、Java、Python、Go、Objective-C 等等;其中支持的Java语言客户端就有多种,下面三个是比较主流的Java客户端程序:

  • Jedis
  • Lettuce
  • Redisson

Jedis

​ 一个比较轻巧的Redis Java客户端,其API提供了比较全面的Redis命令的支持。Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。

Lettuce

​ 高级Redis客户端,用于线程安全的同步,异步和反应式使用。 支持集群,哨兵,管道和编解码器。是SpringBoot2 中默认使用的Redis Java客户端。

Redisson

Redisson 是用于构建分布式应用的 Redis Java客户端。Redisson实现了分布式和可扩展的Java数据结构,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列。

项目地址:https://github.com/redisson/redisson

中文文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

技术选型

​ Jedis 和 Lettuce 对Redis服务的基本操作提供了比较全面的API支持,Redisson 在这方面不如前两者,但Redisson 提供了很多构建分布式应用的特性,所以在实际项目(java)中,我们推荐 Lettuce 和 Redisson混合使用。

​ Spring Data 项目提供了 RedisTemplate ,它对Jedis 和 Lettuce 进行封装,在SpringBoot 项目中我们可以引入 spring-boot-starter-data-redis 模块来轻松操作Redis。

数据类型

Redis支持以下几种常用的数据类型:

  • String 字符串
  • Hash 哈希表
  • List 列表
  • Set 集合
  • SortedSet 有序集合

注:使用 type key 命令可以查看key对应的value 是什么数据类型

字符串

​ 字符串 string 是 Redis 最简单、最常用的数据类型。string 是二进制安全的,意思是Redis的String可以包含任何数据,包括 jpg图片或序列化的对象。

二进制安全:

​ 二进制安全是一种主要用于字符串操作函数相关的计算机编程术语。二进制安全的意思就是,只关心字符串对应的二进制化数据,而不关心具体格式,只会严格的按照二进制的数据存取,不会妄图以某种特殊格式解析数据。

基本操作

常用字符串命令:

  • set key value [ex seconds] [px milliseconds] [nx|xx]: 设置值,返回 ok 表示成功

    • ex seconds:为键设置过期时间(单位:秒)。
    • px milliseconds:为键设置过期时间(单位:毫秒)。
    • nx: 键必须不存在才可以设置成功,用于新增。可单独用 setnx 命令替代
    • xx:与nx相反,键必须存在才可以设置成功,用于更新。可单独用 setxx 命令替代
  • get key:获取值

  • mset key value [key value …]:批量设置值,批量操作命令可以有效提高处理效率

  • mget key [key …]:批量获取值,批量操作命令可以有效提高业务处理效率

  • incr key:自增1,返回结果分 3 种情况:

    • 值不是整数,返回错误。
    • 值是整数,返回自增后的结果。
    • 键不存在,按照值为0自增,返回结果为1。
  • decr(自减1)、 decrby(自减指定数字)、incr(自增1)、incrby(自增指定数字)

  • getrange key start end 范围查找子字符串

应用场景

  1. 缓存,提高查询性能。比如存储登录用户信息、电商中存储商品信息;
  2. 计数器(比如限制一个IP地址在一定时间内对网站的访问次数),短信限流;
  3. 共享 Session,例如:一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可 能会发现需要重新登录,为了解决这个问题,可以使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的,每次用户 更新或者查询登录信息都直接从Redis中集中获取。

哈希表

​ Redis hash 是一个string 类型的 field-value 映射表,和Java中的HashMap 以及 Js 中的 Map 非常相似,内部是无序字典。hash 非常适合用于存储对象,比如用户信息。

基本操作

常用哈希命令

  • hset key field value:设置值
  • hsetnx key field value:设置值(只有当哈希表该字段不存在时才能设置成功,防止覆盖)
  • hget key field:获取值
  • hkeys key:获取所有field
  • hvals key:获取所有value
  • hgetall key:获取所有的field-value
  • hmset key field value [field value …]:批量设置field-value
  • hmget key field [field …]:批量获取field-value
  • hdel key field [field …]:删除field
  • hlen key:计算field个数
  • hexists key field:判断field是否存在
  • hincrby key field increment 给field增加指定值

应用场景

常用于存储对象及其属性信息:

1、由于hash数据类型的key-value的特性,用来存储关系型数据库中表记录,是redis中哈希类型最常用的场景。一条记录作为一个key-value,把每列属性值对应成field-value存储在哈希表当中,然后通过key值来区分表当中的主键。

2、经常被用来存储用户相关信息。优化用户信息的获取,不需要重复从数据库当中读取,提高系统性能。

列表List

​ Redis 中的 List 实现原理是一个双向链表,而不是数组,相当于 Java 中的 LinkedList,这意味着 List的插入和删除很快,时间复杂度为O(1),但是索引定位慢,时间复杂度为O(n)。

基本操作

常用列表命令

  • rpush key value [value …]:从右边插入元素

  • lpush key value [value …]:从左边插入元素

  • linsert key before|after pivot value:向某个元素前或者后插入元素

  • lpop key:从列表左侧弹出元素

  • rpop key:从列表右侧弹出

  • lrem key count value:删除指定元素,lrem命令会从列表中找到等于value的元素进行删除,根据count的不同 分为三种情况:

    • ·count>0,从左到右,删除最多count个元素。
    • count<0,从右到左,删除最多count绝对值个元素。
    • count=0,删除所有。
  • lrange key start end:获取指定范围内的元素列表,lrange key 0 -1可以从左到右获取列表的所有元素

  • lindex key index:获取列表指定索引下标的元素

  • llen key:获取列表长度

  • ltrim key start end:按照索引范围修剪列表

  • lset key index newValue:修改指定索引下标的元素

  • blpop key [key …] timeout 和 brpop key [key …] timeout:阻塞式弹出

应用场景

1、消息队列:reids的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。

2、文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。

使用参考:

  • lpush+lpop=Stack(栈)
  • lpush+rpop=Queue(队列)
  • lpush+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息队列)

集合

​ Redis的 Set 是String类型的无序集合,集合中的元素是唯一的,不可重复。相当于 Java 语言里面的 HashSet 和 JS 里面的 Set。

基本操作

常用集合命令

  • sadd key element [element …]:添加元素,返回结果为添加成功的元素个数
  • srem key element [element …]:删除元素,返回结果为成功删除元素个数
  • smembers key:获取所有元素
  • sismember key element:判断元素是否在集合中,如果给定元素element在集合内返回1,反之返回0
  • scard key:计算元素个数,scard的时间复杂度为O(1),它不会遍历集合所有元素
  • spop key:从集合随机弹出元素,从3.2版本开始,spop也支持[count]参数。
  • srandmember key [count]:随机从集合返回指定个数元素,[count]是可选参数,如果不写默认为1
  • sinter key [key …]:求多个集合的交集
  • suinon key [key …]:求多个集合的并集
  • sdiff key [key …]:求多个集合的差集

应用场景

1、标签:比如我们博客网站常常使用到的兴趣标签,把一个个有着相同爱好,关注类似内容的用户利用一个标签把他们进行归并。

2、共同好友功能,共同喜好,或者可以引申到二度好友之类的扩展应用。

3、统计网站独立IP。利用set集合当中元素不唯一性,可以快速实时统计访问网站的独立IP。

有序集合

​ Redis中的 Zset 和 Set 类似,区别是Zset 内部元素是有序的,Zset 额外提供一个 score 参数(浮点型)来为成员排序,并且插入是有序的,即自动排序。当你需要一个有序的并且不重复的集合,那么可以选择sorted Set。

​ Zset 中成员的值是唯一的,但分数(score)却可以重复。

基本操作

常用有序集合命令

  • zadd key score member [score member …]:添加成员,返回结果代表成功添加成员的个数。

    • nx:member必须不存在,才可以设置成功,用于添加
    • xx:member必须存在,才可以设置成功,用于更新
    • incr:对score做增加,相当于后面介绍的zincrby
  • zincrby key increment member:增加成员的分数

  • zcard key:计算成员个数

  • zscore key member:查看某个成员的分数

  • zrank key member 和 zrevrank key member:计算成员的排名,zrank是从分数从低到高返回排名,zrevrank反之

  • zrem key member [member …]:删除成员

  • zrange key start end [withscores] 和 zrevrange key start end [withscores]:返回指定排名范围的成员,zrange是从低到高返回,zrevrange反之。

  • zrangebyscore key min max [withscores] [limit offset count] 和 zrevrangebyscore key max min [withscores] [limit offset count] 返回指定分数范围的成员,其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之

  • zcount key min max:返回指定分数范围成员个数

注:

​ 有序集合相比集合提供了排序字段,但是也产生了代价,sadd的时间复杂度为O(1),而zadd的时间 复杂度为O(log(n))。

应用场景

  1. 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
  2. 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。

Key的使用规范

Redis中单个Key的存储上限是512M。命令的关键字不区分大小写,key 的名称是区分大小写的;

  1. Key 的命名以英文字母开头,建议只使用小写英文字母、数字、英文点号(.)和冒号(:);
  2. Key 的命名应该具有可读性,不应该含义不清或太短;
  3. Key 不能过长,尽量不要超过1Kb,过大的key不仅消耗过多内存,而且会降低查找效率;
  4. 在一个项目中,Key 应该使用统一的命名规范,例如 user:id:10020:name (表名:主键名:主键值:字段名)。

注:keys * 命令在生产环境下谨慎使用,因为Redis是单线程执行指令,所以在大数据量情况下执行会影响性能;

小结

应用场景汇总

类型 描述 特性 场景
String字符串 最简单、最常用的数据类型,二进制安全 可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512M 1.缓存
2.计数器
3.共享 Session
Hash
字典
string 类型的 field-value 映射表 适合存储对象信息 1.通用对象存储
2.购物信息等
List
列表
用于存储多个元素的容器 底层是一个双向链表,提供从两端分别插入和弹出元素的功能,方便实现栈和队列。 1.热销榜,文章列表
2.工作队列
3.最新列表,如最新评论
Set
集合
集合内元素不重复且无序 为集合提供了求交集、并集、差集等操作 1,.给用户添加标签
2.给标签添加用户
3.社交相关(交集,并集,差集)
ZSet有序集合 将Set中的元素增加一个权重参数score,元素按score有序排列 数据插入集合时,已经自动进行排序 1、排行榜
2、优先队列

数据淘汰策略

Redis服务器的内存容量是有限的,不可能只增不删,因此需要淘汰数据。

Redis 有两种数据淘汰策略:

  • 为数据key设置过期时间;
  • 采用淘汰策略自动淘汰数据;

过期时间

单独设置key过期时间的指令:

# 指定key多少秒后过期
EXPIRE key seconds;
# 指定key多少毫秒后过期
PEXPIRE key milliseconds
# 指定key在时间戳后过期(自1970年1月1日以来的秒数)
EXPIREAT key timestamp
# 指定key在时间戳后过期(自1970年1月1日以来的毫秒数)
PEXPIREAT key timestamp

如果设置成功返回 1,如果key不存在返回0 ;

当然你也可以在设置值时同时指定过期时间,并且可以保证设值和设过期时间的原子性。

# 设置值时指定过期时间
SET key value EX 60;

设置了有效期后,可以通过 ttl 和 pttl 两个命令来查询剩余过期时间:

# 返回 key 剩余过期秒数
ttl key 
# 返回 key 剩余过期毫秒数
pttl key 

过期策略

删除一个过期的键,一般有三种策略:

  • 定时删除:即每个键设置一个定时器,到期自动删除;(缺点是占用过多的CPU计算资源)
  • 惰性删除:每次判断获取键时判断是否过期,如果过期就删除;(缺点是占用过多的内存资源)
  • 定期扫描:系统每隔一段时间就定期扫描一次设置过期时间的键,如果过期就删除;(缺点是可能返回已经过期的键)

​ Redis采用的是 惰性删除 + 定期扫描 的方式淘汰过期的键,也就是策略2和策略3结合使用。Redis只会定期扫描设置过期时间的键,不会扫描所有键。

淘汰策略

​ 通过 maxmemory 命令或配置文件可以设置 Redis 最大使用内存,如果Redis 内存满了,此时继续写入数据,默认情况下,Redis会内存溢出并报错,如果不想内存溢出,可以选择Redis 提供的内存淘汰策略来解决。

​ Redis 提供以下几种淘汰策略:

  • volatile-lru :在已设定过期时间的数据中,删除最近最少使用的数据;
  • allkeys-lru : 在所有key中,删除最近最少使用的数据,这是应用最广泛的策略;
  • volatile-random :在已设定过期时间的数据中,随机删除;
  • allkeys-random :在所有key中,随机删除数据;
  • volatile-ttl :在已设定过期时间的数据中,排序然后删除最快过期的数据;
  • noeviction :如果设置该属性,则不会进行删除操作,如果内存溢出则报错返回(默认策略);
  • volatile-lfu :在已设定超时时间的数据中,删除最少使用的数据;
  • allkeys-lfu :在所有key中,删除最少使用的数据;

注:LRU (Least Recently Used最近最少使用算法)和 LFU (最少使用算法)都是页面置换算法,LRU是Redis最早支持的内存淘汰策略,LFU是Redis5.0版本引入的。

Redis的默认策略是 noeviction ,即不会淘汰数据,只是在写操作时返回错误。

高级进阶

除了常用数据类型的支持,Redis还支持多种功能特性:

  • 事务
  • Lua脚本
  • 管道
  • 发布订阅
  • Stream
  • Geo地理位置
  • HyperLogLog
  • 布隆过滤器

事务

​ 许多情况下我们需要一次执行多个命令,而且需要其同时成功或者失败。为了保证多个命令组合的原子性,Redis 提供了简单的事务功能以及集成 Lua 脚本来解决这个问题。

​ Redis 提供了简单的事务功能,将一组需要一起执行的命令放到 multiexec 两个命令之间。Multi 命令代表事务开始,exec 命令代表事务结束,它们之间的命令是原子顺序执行的(按顺序依次执行,中间不允许执行任何其他命令)。

事务相关命令:

MULTI		# 开启事务;
EXEC    	# 执行事务块内的所有命令
DISCARD 	# 取消事务块内的所有命令
WATCH		# 监视一个/多个key,如果在事务执行之前这个key被修改,那么事务将被打断;
UNWATCH		# 取消所有Key的监视;

示例:

> multi
OK
> SET msg "hello world"
QUEUED
> GET msg
QUEUED
> EXEC
1) OK

Redis事务错误处理分两种情况:

  1. 加入命令队列的过程中,发生报错(如命令输入错误),则整个命令队列都会被取消,事务结束;
  2. 执行命令队列时,发生报错,则只是报错的命令不会执行,其他命令正常执行;

​ Redis 提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,主要有以下几点:

  1. 不够满足原子性。一个事务执行过程中,其他事务或 client 是可以对相应的 key 进行修改的(并发情况下,例如电商常见的超卖问题),想要避免这样的并发性问题就需要使用 WATCH 命令,但是通常来说,必须经过仔细考虑才能决定究竟需要对哪些 key 进行 WATCH 加锁。然而,额外的 WATCH 会增加事务失败的可能,而缺少必要的 WATCH 又会让我们的程序产生竞争条件。
  2. 后执行的命令无法依赖先执行命令的结果。由于事务中的所有命令都是互相独立的,在遇到 exec 命令之前并没有真正的执行,所以我们无法在事务中的命令中使用前面命令的查询结果。我们唯一可以做的就是通过 watch 保证在我们进行修改时,如果其它事务刚好进行了修改,则我们的修改停止,然后应用层做相应的处理。
  3. 事务中的每条命令都会与 Redis 服务器进行网络交互。Redis 事务开启之后,每执行一个操作返回的都是 queued,这里就涉及到客户端与服务器端的多次交互,明明是需要一次批量执行的 n 条命令,还需要通过多次网络交互,显然非常浪费(这个就是为什么会有 pipeline 的原因,减少 RTT 的时间)。

针对于Redis 的事务缺陷,可以通过执行 Lua 脚本来解决。

Lua脚本

​ Lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Redis 通过内嵌支持 Lua 环境,使用内置的解释器来执行Lua 脚本。

​ 使用 eval 命令执行Lua脚本,eval 语法::

eval script numkeys key [key ...] arg [arg ...]

其中:

  • script 一段 Lua 脚本或 Lua 脚本文件所在路径及文件名
  • numkeys Lua 脚本对应参数数量
  • key [key …] Lua 中通过全局变量 KEYS 数组存储的传入参数
  • arg [arg …] Lua 中通过全局变量 ARGV 数组存储的传入附加参数

示例:

EVAL ./release.lua 1 my:lock lock:394A23CC43

release.lua 脚本:如果指定键的值与入参值相同,则删除该键值。

if redis.call('get',KEYS[1]) == ARGV[1] then
    return redis.call('del',KEYS[1])
else
    return 0
end

下面是一些常用的Lua 语法示例,更过Lua语法请参考官网:

-- 定义一个局部变量val,并输出
local val = "hello"
print(val)
-- 定义一个数组,输出第一个值,下标从1开始
local myTable = { "redis", "lua", true, 8 }
print(myTable[1])
-- 使用for循环计算0到10的和
local num = 0
for i = 1, 10 do
    num = num + i
end
-- 使用while 计算0到10的和
local sum = 0
local i = 0
while i <= 10 do
    sum = sum + i
    i = i + 1
end
-- 遍历数组,#myTable2 用于取数组的大小。if-then-else-end 判断
local myTable2 = { "python", "redis", "java" }
for i2 = 1, #myTable2 do
    if myTable2[i2] == "redis" then
        print("true")
        break
    else
        print("false")
    end
end
-- 函数定义:函数以function开头,以end结尾
function add(param1, param2)
    return param1 + param2
end

管道技术

​ Redis 提供了一些批量操作命令(例如mget、mset等),有效地节约RTT。但大部分命令是不支持批量操作的,例如要执行 n 次 hgetall 命令,并没有 mhgetall 命令存在,需要消耗 n 次 RTT。

​ Redis 的客户端和服务端一般部署在不同的机器上。例如客户端请求服务器的 RTT(往返时延)为13 毫秒,那么客户端在 1 秒内只能执行 80 次左右的命令,这个和 Redis 的高并发高吞吐特性背道而驰。Pipeline(流水线)机制能改善上面这类问题,它能将一组 Redis 命令进行组装,通过一次网络请求传输给 Redis,再将这组 Redis 命令的执行结果按顺序返回给客户端。

不使用 Pipeline 的命令执行流程:

redis-pipline1

使用 Pipeline 的命令执行流程:

redis-pipline2

下面使用 Spring Data 提供的 redisTemplate 写一个示例:

public List testPipeline(){

    List list = redisTemplate.executePipelined(new SessionCallback<Object>() {
        @Override
        public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
            ValueOperations<String, Object> valueOperations = (ValueOperations<String, Object>) operations.opsForValue();
            valueOperations.set("yzh1", "hello world");
            valueOperations.set("yzh2", "redis");
            valueOperations.get("yzh1");
            valueOperations.get("yzh2");
            return null;  // 返回null即可,因为返回值会被管道的返回值覆盖,外层取不到这里的返回值
        }
    });
    return list;
}

发布订阅

​ 发布订阅(pub/sub)是一种消息通信模式:发送者发送消息,订阅者接收消息。使用pub/sub主题订阅者模式,可以实现 1:N 的消息队列。

​ Redis客户端可以订阅任意数量的频道。发布的消息不会持久化,没有订阅者时候,发布消息会丢失,当在发布消息之后对channel进行订阅不会收到之前发布的消息。

常用命令:

SUBSCRIBE channel...  		# 订阅指定的一个/多个频道
PSUBSCRIBE pattern...   	# 订阅指定的一个/多个模式的频道

PUBLISH channel message		# 将消息发布到指定频道

UNSUBSCRIBE channel...		# 退订给定的频道
PUNSUBSCRIBE pattern...   	# 退订指定模式的频道

订阅模式频道:

​ 每个模式以* 作为通配符, 比如 user* 表示订阅所有以user开头的频道(如 user.log、user.news、user.message….)

Stream

​ Redis Stream 是 Redis 5.0 版本新增加的数据结构。Redis Stream 主要用于消息队列(MQ),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。

​ 简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

GEO 地理位置

​ Redis 提供了 GEO(地理信息定位)功能,支持存储地理位置信息,用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。

GEO 相关的命令如下:

# 用于添加城市的坐标信息:longitude、latitude、member分别是该地理位置的经度、纬度、成员
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang
# 获取地理位置信息
geopos key member [member ...]
# 获取两个坐标之间的距离,unit代表单位:m 米,km 千米
geodist key member1 member2 [unit]
# 获取指定位置范围内的地理信息位置集合,此命令可以用于实现附近的人的功能
georadius key longitude latitude radiusm
georadiusbymember key member radiusm
# 获取geo hash
geohash key member [member ...]
# 删除操作,GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除。
zrem key member

georadius 和g georadiusbymember两个命令的作用是一样的,都是以一个地理位置为中心算出指定半径内的其他地理信息位置,不同的是georadius命令的中心位置给出了具体的经纬度,georadiusbymember只需给出成员即可。其中radiusm|km|ft|mi是必需参数,指定了半径(带单位),这两个命令有很多可选参数,参数含义如下:

  • withcoord:返回结果中包含经纬度。
  • withdist:返回结果中包含离中心节点位置的距离。
  • withhash:返回结果中包含geohash,有关geohash后面介绍。
  • COUNT count:指定返回结果的数量。
  • asc|desc:返回结果按照离中心节点的距离做升序或者降序。
  • store key:将返回结果的地理位置信息保存到指定键。
  • storedist key:将返回结果离中心节点的距离保存到指定键。

Redis使用geohash将二维经纬度转换为一维字符串,geohash有如下特点:

  • GEO的数据类型为zset,Redis将所有地理位置信息的geohash存放在zset中。
  • 字符串越长,表示的位置更精确,表3-8给出了字符串长度对应的精度,例如geohash长度为9时,精度在2米左右。
  • 两个字符串越相似,它们之间的距离越近,Redis利用字符串前缀匹配算法实现相关的命令。
  • geohash编码和经纬度是可以相互转换的。
  • Redis正是使用有序集合并结合geohash的特性实现了GEO的若干命令。

HyperLogLog

HyperLogLog,它是 LogLog 算法的升级版,作用是能够在大数据中做高效率的基数统计(去重计数)。

有如下特点:

  • 能够使用极少的内存统计海量数据,在Redis中实现的 HyperLogLog ,只需要12K 内存就可以统计2^64^个数据。
  • 基数统计存在一定误差,标准误差率低于1%(0.81%)。
  • 误差可以通过 辅助计算因子进行降低。

LogLog 算法

​ 基数估计算法(LogLogCounting)是基于概率论与数理统计所设计的概率统计算法,用于在大数据中进行不完全精确的基数统计。

​ 为什么叫LogLog 算法呢?因为这种算法的**空间复杂度是O(log(log(Nmax)))**,可以通过KB级内存估计数亿级别的基数。

有一个网站,可以动态地让你观察到 HyperLogLog 的算法到底是怎么执行的:

image-20200315092007865

位图BitMap

​ 许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。Redis提供了Bitmaps这个类型可以实现对位的操作:

  • Bitmaps本身不是一种数据结构,它实际上是字符串,但是它可以对字符串的位进行操作。
  • Bitmaps单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在 Bitmaps 中叫做偏移量。

​ 在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录, 签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位, 365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。

语法:

setbit key offset value  	# 设置或者清空 key 的 value(字符串)在 offset 处的 bit 值
getbit key offset  			# 返回 key 对应的 string 在 offset 处的 bit 值 
bitcount key [start end] 	# start end 范围内被设置为1的数量,不传递 start end 默认全范围

使用案例,统计用户登录(活跃)情况

# userId=001的用户登录,这是今天登录的第一个用户。
setbit userLogin:2021-03-10 001 1 
# userId=002的用户登录,这是今天第二个登录的用户。
setbit userLogin:2021-03-10 002 1 

# 查询用户002在2021-04-10 有没有登录
getbit userLogin:2021-03-10 002
# 统计2021-03-10当天登录系统的用户数量
bitcount active:2021-03-10

​ 由于 bit 数组的每个位置只能存储 0 或者 1 这两个状态;所以对于实际生活中,处理两个状态的业务场景就可以考虑使用 bitmaps。如用户登录/未登录,签到/未签到,关注/未关注,打卡/未打卡等。同时 bitmap 还通过了相关的统计方法进行快速统计。

布隆过滤器

​ 布隆过滤器(Bloom Filter)是1970年布隆提出来的。它实际上是一个很长的二进制向量和一系列随机映射函数(哈希函数)。

​ 布隆过滤器用于检索一个元素是否在一个集合中。它的优点是空间和时间效率都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

应用场景

  • 大数据中判断某个元素是否存在
  • 解决缓存穿透
  • 爬虫、邮箱系统的过滤

平时有没有注意到一些正常的邮件也会被放进垃圾邮件目录中,这就是布隆过滤器的误判导致的。

Redis 布隆过滤器

RedisBloom 在Redis4.0 中以插件的形式提供布隆过滤器功能,布隆过滤器是一个概率数据结构。

BloomFilter 有两个基本操作,即 bf.add 添加元素和 bf.exists 查询元素。

​ 如果使用 BloomFilter 之前不指定过滤器,那么会使用Redis默认的布隆过滤器,Redis 也提供自定义参数的布隆过滤器。只需要在 bf.add 之前使用 bf.reserve 指令显示的创建自定义布隆过滤器;

bf.reserve 有三个参数: key , error_rate , initial_size;

  • error_rate 错误率越低,需要的空间越大;对于不需要过于精确的场合,设置稍大一些也没有关系。
  • initial_size 表示初始化大小,当实际数量超过这个值时,错误率就会上升。

​ 如果不使用自定义布隆过滤器, 默认的配置 error_rate= 0.01 ,initial_size =100 ;布隆过滤器支持 add 和 isExist 操作,不支持 delete 操作

布隆过滤器判断

  • 当mightContain() 方法返回 true 时,则该元素有99%的可能性在布隆过滤器中;

  • 当mightContain() 方法返回 false 时,则该元素100% 不在布隆过滤器中;

注:Google的核心类库Guava也实现了布隆过滤器BloomFilter功能。

内部编码

​ Redis 的数据类型有:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)等。但这些只是Redis 对客户端的数据类型,实际上每种数据类型都有自己底层的内部编码实现,而且是多种实现, Redis 会在适当的场景选择合适的内部编码。

Redis内部编码2

注:上图中关于list的内部编码在Redis 3.2 版本中已经进行优化,使用quicklist 作为list 的内部编码;

​ 可以看到每种数据结构都有多种内部编码实现,例如 字符串类型包含了 embstr raw int 三种内部编码。同时,有些内部编码如 ziplist, 可以作为多种外部数据结构的内部实现,可以通过 object encoding 命令查询内部编码。

​ Redis 所有的数据结构都是以唯一的字符串key 作为名称,然后通过key 来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。

Redis这样设计有两个好处:

  1. 在不影响对外数据结构和命令的情况下改进内部编码,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令。
    这种设计理念和编程中的抽象接口与实现类是一样的,我们定义一个接口,但可以有多种不同的实现。
  2. 多种内部编码实现可以在不同场景下发挥各自的优势。例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis会根据配置选项将列表类型的内部实现转换为其他数据结构。

整体存储结构

​ 在学习底层数据存储结构之前,首先看下redis整体的存储结构。redis内部整体的存储结构是一个大的hashmap,内部是数组+链表的方式实现,通过拉链法解决hash冲突,每个dictEntry为一个key/value对象,value为定义的redisObject

结构图如下:

image-20230227174321394

接着再往下看redisObject究竟是什么结构

image-20230227175157522
  • *ptr指向具体的数据结构的地址。
  • type表示该对象的类型,即String,List,Hash,Set,Zset中的一个,但为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种。
  • encoding 表示对象底层所使用的编码。

下面会分别介绍5种数据结构的内部编码方式。

String

字符串类型的内部编码有三种,Redis会根据当前值的类型和长度决定使用何种内部编码实现:

  • embstr:不大于44个字节的短字符串。
  • raw:大于44个字节的长字符串。
  • int:8个字节的长整型。

示例:

# 短字符串使用embstr编码
> set a_string_short aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-
OK

> STRLEN a_string_short
(integer) 44

> object encoding a_string_short
"embstr"
# 当字符串长度大于44时就变成了raw
> set b_string_short aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-aaaaaaaaaa-a
OK

127.0.0.1:6379> STRLEN b_string_short
(integer) 45

127.0.0.1:6379> object encoding b_string_short
"raw"

redisObject 和 sdshdr8 结构体:

image-20230227195631172

为什么会是44个字节呢?

​ 在64位操作系统中,cpu缓存行大小占64byte,而 redisObject 和 sdshdr8 正好占用20个字节,所以当业务数据大小在64-20=44字节之内的话,可以利用cpu缓存行特性:操作系统分配内存的时候,就会挨着redisObject进行分配,开辟一块连续的空间存储,利用cpu的缓存行一次读取到数据,减少内存IO,这样整个数据就在cpu缓存行范围内,在进行数据读取的时候,通过redisObject我们可以直接拿到值,而不用通过指针再一次寻址去拿数据,这就是embstr做的事情。

此外,使用append 等命令会修改redis内部编码,就不适用cpu缓存行优化的方式了:

> set a a
OK

> object encoding a
"embstr"

> append a b
(integer) 2

> object encoding a
"raw"

raw 编码

# 长字符串使用raw编码
 set str "The sun is at the end of the mountain, the Yellow River flows into the sea"
object encoding str   # "raw"  

int 编码

​ 当键值内容可以用一个64位有符号整数表示时,Redis会将键值转换成long类型来存储。如SET key 123456,实际占用的空间是sizeof(redisObject) 为16字节,比存储 “foobar” 节省了 一半的存储空间。

# 整型类型使用int编码
set str 1234567 
object encoding str   # "int" 

注:

​ string 类型使用的内部编码是固定规则,不能通过配置文件去修改规则。其他四种类型都可以在配置文件中修改内部编码规则。

Hash

哈希类型的内部编码有两种:

  • ziplist(压缩列表):当散列类型键的字段个数少于 hash-max-ziplist-entries参数值(默认512个)且每个字段名和字段值的长度都小于hash-max-ziplist-value参数值(默认64个字节)时,Redis会使用ziplist作为哈希的内部实现。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。

  • hashtable(哈希表):当无法满足以上条件时,Redis会使用 hashtable作为哈希的内部实现。因为当数据量变大时 ziplist的读写效率会下降,而hashtable 的读写时间复杂度为O(1)。

示例:

# 当field个数比较少且没有大的value时,内部编码为ziplist
hmset user:1 name kebi age 26
object encoding user:1			# "ziplist" 

# 当有value大于64个字节,内部编码会由ziplist变为hashtable
hmset user:1 info "The sun is at the end of the mountain, the Yellow River flows into the sea"
object encoding user:1	    # "hashtable"

# 当field个数超过512,内部编码也会由ziplist变为hashtable
...

注意:当一个哈希的编码由ziplist变为hashtable的时候,即使在替换掉所有值,它也一直都会是hashtable类型,不会转化为ziplist。

与Hash内部编码相关的配置项:

# hash类型使用ziplist编码时,字段的最大个数
hash-max-ziplist-entries 512
# hash类型使用ziplist编码时,每个字段名和字段值的最大长度(字节)
hash-max-ziplist-value 64 

List

Redis 3.2之前的版本 ,列表类型的内部编码有两种:

  • ziplist(压缩列表):如果列表的元素个数小于512个,同时每个元素的值小于64字节时 ,采用ziplist为内部编码保存。

  • linkedlist(链表):以上条件无法满足时,Redis会使用 linkedlist作为列表的内部实现。

ziplist 结构:

image-20230227094150183

ziplist数据结构说明:

  • zlbytes:32bit表示ziplist占用的字节总数
  • zltail:32bit表示ziplist表中最后一项entry在ziplist中的偏移字节数。通过zltail我们可以很方便地找到最后一项,从而可以在ziplist尾端快速地执行push或pop操作
  • zlen:16bit表示ziplist中数据项entry的个数
  • entry:表示真正存放数据的数据项,长度不定
  • zlend:ziplist最后一个字节,是一个结束标记,值固定等于255
  • prerawlen:前一个entry的数据长度
  • len:entry中数据的长度
  • data:真实数据存储

为了节省内存空间,ziplist是非常紧凑的一种数据类型。而非常紧凑的数据结构的缺点是:

  • 空间必须是连续的
  • 数据量非常大的时候往里面加元素,数据迁移很麻烦
  • 频繁的内存分配与释放是不划算的,所以redis针对这个问题进行了优化,提供了quicklist

Redis3.2版本提供了quicklist 编码,快速列表是ziplist和linkedlist的混合体,是将linkedlist按段切分,每一段用ziplist来紧凑存储,多个ziplist之间使用双向指针链接,它结合了ziplist和linkedlist两者的优势。

为什么不直接使用linkedlist?

​ linkedlist的附加空间相对太高,prev和next指针就要占去16个字节,而且每一个结点都是单独分配,会加剧内存的碎片化,影响内存管理效率。

image-20230227094211727

示例:

> lpush queue-task a b c
OK
> type queue-task
list
> object encoding queue-task
"quicklist"

ziplist容量

quicklist内部默认单个ziplist长度为8kb,超出了这个字节数,就会新建一个ziplist。关于长度可以使用list-max-ziplist-size 设置。

list-max-ziplist-size 配置项用于指定list中单个 ziplist 的最大容量,官方给出以下几种选项:

  • -5: 最大 64 Kb <–不推荐用于正常工作负载
  • -4: 最大: 32 Kb <– 不推荐
  • -3: max size: 16 Kb <– 不推荐
  • -2: max size: 8 Kb <– 推荐
  • -1: max size: 4 Kb <– 推荐

默认选项为 -2,官方认为性能最高的选项通常是 -2(8 Kb 大小)或 -1(4 Kb 大小);

压缩深度

上面说到了quicklist下是用多个ziplist组成的,同时为了进一步节约空间,Redis还会对ziplist进行压缩存储,可以选择压缩深度。list-compress-depth 配置项用于设置 quickList的数据压缩范围,提升数据存取效率:

  • 0:默认不压缩
  • 1:表示不压缩头尾节点,压缩中间数据 (为了支持快速push/pop操作
  • 2:表示头尾节点和头尾相邻的一个节点不压缩,压缩除此之外中间的
  • 3:表示头尾节点和头尾相邻的两个节点不压缩,压缩除此之外中间的
  • …:依次类推
# 单个 ziplist 的最大容量(默认8Kb)
list-max-ziplist-size -2
# 数据压缩范围(默认不压缩)
list-compress-depth 0

Set

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries 配置(默认512个)时,Redis会选用intset来作为集合内部实现,从而减少内存的使用。

  • hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用 hashtable作为集合的内部实现。

示例:

# 当元素个数较少且都为整数时,内部编码为intset:
sadd setkey 2 3 4 5
object encoding setkey		# "intset"

# 当元素个数超过512个,内部编码变为hastable:
sadd setkey2 1 2 3 ... 513
object encoding setkey2		#"hashtable"

# 当某个元素不为整数时,内部编码为hashtable:
sadd setkey3 a b c
object encoding setkey2		#"hashtable"

与ZSet内部编码相关的配置项:

# set类型使用intset编码的最大元素个数(必须为整数)
set-max-intset-entries 512

ZSet

有序集合类型的内部编码有两种

  • ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist-entries配置(默认128个)

    同时每个元素的值小于zset-max-ziplist-value配置(默认64个字节)时,Redis会用ziplist来作为有序集合的内部实现, ziplist可以有效减少内存使用。

  • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist 的读写效率会下降。

示例:

# 当元素个数较少且每个元素较小时,内部编码为ziplist:
zadd zsetkey 50 a 60 b 30 c
object encoding zsetkey		# "ziplist"

# 当元素个数超过128个,内部编码变为skiplist:
zadd zsetkey 1 2 3 ... 129
object encoding zsetkey		# "skiplist"

# 当某个元素大于64个字节时,内部编码也会变为skiplist:
zadd zsetkey 50 a "The sun is at the end of the mountain, the Yellow River flows into the sea"
object encoding zsetkey		# "skiplist"

与ZSet内部编码相关的配置项:

# zset类型使用ziplist编码的最大元素个数
zset-max-ziplist-entries 128 
# zset类型使用ziplist编码时,单个元素的最大容量(字节)
zset-max-ziplist-value 64 

数据备份与恢复

Redis 服务支持数据持久化。

RDB与AOF同时开启 默认先加载AOF的配置文件;4.0+的可以使用RDB-AOF混合持久化格式。

RDB 内存快照

将某一时刻的内存数据,以二进制文件的形式写入磁盘。

手动执行方式

手动通过命令执行数据持久化:

# 同步保存数据到磁盘
127.0.0.1:6379> save	
OK
# 异步保存数据到磁盘
127.0.0.1:6379> bgsave	
Background saving started

注:

bgsave 是Redis通过调用 fork 来创建一个子进程,子进程负责快照写入磁盘,而父进程仍然继续处理命令;

save 则是同步持久化数据,save 命令执行过程中,Redis不能响应其他任何命令;

配置文件方式

在redis.conf中设置save配置选项(常用)

save 900 1
save 300 10
save 60 10000

​ 上面是Redis默认的配置信息,表示 900s (15min) 内至少发生1次写操作,则触发异步持久化; 300s (5min) 内至少发生10次写操作,则触发异步持久化; 60s (1min) 内至少发生10000次写操作,则触发异步持久化;

​ 任何一条满足,都会触发异步持久化( BGSAVE 操作),在根目录生成持久化文件。

和RDB 相关的配置还有如下

# RDB 文件名
dbfilename dump.rdb
# RDB 文件目录
dir ./
# bgsave 失败之后,是否停止持久化数据到磁盘,yes 表示停止持久化,no 表示忽略错误继续写文件。
stop-writes-on-bgsave-error yes
# RDB 文件压缩
rdbcompression yes
# 写入文件和读取文件时是否开启 RDB 文件检查,检查是否有无损坏,如果在启动是检查发现损坏,则停止启动。
rdbchecksum yes

RDB 文件恢复

​ 当Redis 服务器启动时,如果根目录存在RDB文件 dump.rdb,Redis就会自动加载RDB文件还原持久化数据,如果根目录没有,请先将dump.rdb文件移动到Redis根目录。

优缺点

  • RDB文件的内容为二进制数据,占用内存更小,更适合做备份文件。
  • RDB 对灾难恢复很有用,它是一个紧凑的文件,可以更快的传输到远程服务器进行数据恢复。
  • RDB可以更大程度的提高Redis的运行速度,因为每次持久化时主进程会fork 一个子进程进行工作,Redis主进程并不会执行磁盘IO操作。
  • 与AOF格式的文件相比,RDB文件可以更快的重启。

缺点:

  • 在意外宕机情况下,RDB文件会丢失一部分数据。(RDB保存的是某一时刻的内存数据)
  • 数据量很大时,持久化很耗时;

AOF 日志文件

​ AOF 持久化方式默认是关闭的。

​ 在执行写命令时,AOF 会将写的命令追加到AOF文件的末尾,以此记录数据的变化。

# redis默认关闭AOF机制,可以将no改成yes实现AOF持久化
appendonly no
# AOF文件名
appendfilename "appendonly.aof"
# AOF持久化同步频率
# appendfsync always	always表示每次写数据都要同步到磁盘,此方式会降低Redis的性能,不建议开启
appendfsync everysec	# everysec表示每秒执行一次同步
# appendfsync no	# no表示让操作系统来决定应该何时进行同步fsync,Linux系统往往可能30秒才会执行一次

rewrite

混合持久化

Redis4.0 新增的特性,混合持久化方式结合了RDB和AOF 的优点,

# 开启混合持久化
aof-use-rdb-preamble yes

正常、异常关闭服务

​ 当Redis通过shutdown命令关闭服务器请求时,会执行 SAVE 命令创建一个快照,如果使用 kill -9 PID 将不会创建快照。

服务高可用

多台服务器组成的集群自然要比单台服务器的可用性更高,Redis 支持三种集群部署模式:

  1. 主从模式 Master-Salve
  2. 哨兵模式 Sentinel
  3. 集群模式 Cluster

主从复制(Master-Slave)

​ 客户端向主服务器(Master)写入数据,自动同步到从服务(Slave)上。

​ Redis的主从复制是异步复制,异步分为两个方面,一个是master服务器在将数据同步到slave时是异步的,因此master服务器在这里仍然可以接收其他请求,一个是slave在接收同步数据也是异步的。

优点:

  • 读写分离,负载均衡。主节点负载读写,从节点负责读,提高服务器并发量
  • 故障恢复,避免单点故障带来的服务不可用
  • 高可用基础,是哨兵机制和集群实现的基础

配置主从复制

Redis的主从配置非常简单,我们假设Redis的master服务器地址为192.168.0.101。

slave服务器配置主服务器

slaveof 192.168.1.101 6379

可以用户命令info replication查看复制信息

Slave默认为只读的

Redis2.6以后,slave只读模式是默认开启的,我们可以通过配置文件中的slave-read-only选项配置是否开启只读模式:

# 默认是yes
slave-read-only yes/no 

复制验证

如果我们需要slave对master的复制进行验证,可以在master中配置requirepass <password>选项设置密码

那么需要在从服务器中使用该密码,可以使用命令config set masterauth <password>,或者在配置文件中设置masterauth <password>

哨兵模式(Sentinel)

​ 在主从模式下,如果Master 宕机,可以将一个Slave 升级为主服务器,以便继续提供服务,但这个过程需要人工手动处理。为此,Redis提供哨兵模式来实现自动化的系统监控和故障恢复功能。

​ 哨兵模式主要做两件事:

  • 监控Master 和 Slave 是否正常运行;
  • Master 故障后自动将一个 Slave 升级为主服务器;

哨兵模式是基于主从复制的,主从复制的优点,哨兵模式都具备。

集群模式(Cluster)

​ Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

​ Redis Cluster 是 Redis 的分布式解决方案,在 3.0 版本正式推出,有效地解决了 Redis 分布式方面的需求。

​ Redis Cluster 节点数量至少为 6 个才能保证组成完整高可用的集群,其中三个为主节点,三个为从节点。三个主节点会分配槽,处理客户端的命令请求,而从节点可用在主节点故障后,顶替主节点。

image-20230221113728931

​ 如上图所示,该集群中包含 6 个 Redis 节点,3主3从,分别为M1,M2,M3,S1,S2,S3。除了主从 Redis 节点之间进行数据复制外,所有 Redis 节点之间采用 Gossip 协议进行通信,交换维护节点元数据信息。

​ 一般来说,主 Redis 节点会处理 Clients 的读写操作,而从节点只处理读操作。

​ Cluster 与主从复制和哨兵的主要区别是,哨兵和主从复制模式的每个节点都是全量数据的存储,而Cluster 可以将数据分布式存储,即每个节点存储的数据都不一样。

​ Redis-Cluster 采用无中心化的结构,特点如下:

  • 所有的Redis节点彼此互联,可以通信;
  • 节点的fail 是通过集群中超过半数的节点检测失败时才生效;
  • 客户端只需要连接Redis集群中任意一个节点即可,不需要中间层代理,不需要连接所有节点;

使用集群,只需要将每个数据库节点的 cluster-enable 配置打开即可。每个集群中至少需要三个主数据库才能正常运行。

cluster-enabled yes  				# 开启集群
cluster-config-file nodes-6382.conf # 指定集群配置文件名

Gossip 通信机制

​ 为了让让集群中的每个实例都知道其他所有实例的状态信息,Redis 集群规定各个实例之间按照 Gossip 协议来通信传递信息。

1)消息的延迟

​ 由于 Gossip 协议中,节点只会随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网的,因此使用 Gossip 协议会造成不可避免的消息延迟。不适合用在对实时性要求较高的场景下。

2)消息冗余

​ Gossip 协议规定,因此就不可避免的存在消息重复发送给同一节点的情况,造成了消息的冗余,同时也增加了收到消息的节点的处理压力。而且,由于是定期发送,因此,即使收到了消息的节点还会反复收到重复消息,加重了消息的冗余。

分布式锁

​ 在许多场景下,不同的进程需要对共享资源以互斥的方式访问,以保证数据安全,分布式锁便是强有力的保障。

单节点Redis分布式锁

​ 整个逻辑就是,让所有想操作「共享资源」的客户端都去 RedisSETNX 同一条数据,谁先添加成功谁就算拥有分布式锁,然后就可以操作「共享资源」,操作完后在删掉该数据,释放锁。

加锁

SET lock_key $unique_id EX 30 NX

注:

​ NX 的意思是不存在时才会设置它的值,否则什么也不做,这保证读改写3个操作在Redis服务器中是原子操作;

​ 设置过期时间30S,是为了防止加锁成功后的客户端还没释放锁就自己宕机,导致锁无法释放;

​ 设置的value是客户端自己知道的随机值或线程ID,其他客户端不知道,这样可以避免客户端释放不属于他的锁;

释放锁

示例:

EVAL ./release.lua 1 my:lock lock:394A23CC43

release.lua 脚本:如果指定键的值与入参值相同,则删除该键值。

if redis.call('get',KEYS[1]) == ARGV[1] then
    return redis.call('del',KEYS[1])
else
    return 0
end

避免锁提前释放

​ 到现在还有一个问题没解决,那就是如果业务执行时间超过锁的过期时间,锁就会在业务执行完之前提前释放,可能导致其他客户端也获取锁,造成线程不安全。

​ 我们可以设计这样一个方案:加锁时,先设置一个过期时间,然后开启一个「守护线程」,定时去检测这个锁的过期时间,如果锁快要过期了,操作共享资源还未完成,那么就对锁进行「自动续期」,重新设置过期时间。如果你是 Java 技术栈,已经有一个 Redisson 库把这些工作都封装好了。

Redisson封装了很多易用的分布式功能:

  • 可重入锁
  • 乐观锁
  • 公平锁
  • 读写锁
  • Redlock

多节点Redis分布式锁

集群下的问题

来看下面这个场景

  1. 客户端 1 在主库上执行 SET 命令,加锁成功
  2. 此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的
  3. 从库被哨兵提升为新主库,这个锁在新的主库上,丢失了!

​ 可以看到在主从切换时,上面的单节点解决方案是有问题的。Redis官方提供了一种规范的算法来实现Redis 多节点的分布式锁 RedLock

RedLock

RedLock 解决方案基于 2 个前提:

  1. 只使用主库,不需要从库和哨兵实例
  2. 主库要部署多个,官方推荐至少 5 个实例

也就是说,想用使用 Redlock,至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系。

注:RedLock 不强调部署 Redis Cluster,部署 5 个不相关的 Redis 实例即可,当然RedLock 和集群部署也并不冲突。

Redlock 整个流程如下:

  1. 客户端先获取「当前时间戳 T1」
  2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),并设置超时时间(毫秒级),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
  3. 如果客户端从超过半数的 Redis 实例(>=3 )加锁成功,则再次获取「当前时间戳 T2」,判断如果锁的租期 > T2 - T1 ,则认为客户端加锁成功,否则认为加锁失败。
  4. 加锁成功后去操作共享资源。
  5. 加锁失败或操作结束,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)

​ 整个逻辑说白了就是大家都去抢注锁,谁抢注的多谁就拥有锁。Redlock 通过向当前存活的多个主库抢注锁,实现了即使部分节点不可用,也不会影响到分布式锁系统,解决主从切换时,锁丢失的问题。

注意 Redlock 依然需要搭配 Redisson 来解决锁提前释放的问题

RedLock的争议

​ 那么 Redlock 真的绝对安全吗?有人并不这么认为。Redis 作者提出的 Redlock 方案后,马上受到英国剑桥大学、业界著名的分布式系统专家 Martin (《数据密集型应用系统设计》作者)的质疑!认为这个 Redlock 的算法模型是有问题的,并写了篇文件对分布式锁的设计,提出了自己的看法。

​ 之后,Redis 作者 Antirez 面对质疑,不甘示弱,也写了一篇文章,反驳了对方的观点,并详细剖析了 Redlock 算法模型的更多设计细节。关于争议的具体细节不在本文详细描述了,但强烈建议大家去仔细读一读,看看神仙是怎么“打架”的。

​ 最后比较中肯的结论是,无论是 RedLock还是其他分布式锁的算法,在极端情况下都有可能出现问题, 只要明确极端情况下应该怎么提前采取措施避免,RedLock 当然是可以使用的。印证了那句话,在软件领域,没有一个解决方案是万能通用的,只有在不同的场景下选择合适的解决方案。同时,对于分布式锁的争议本身这件事,也会加深我们对分布式锁的理解。

运维

# 连接服务器
redis-cli -h host -p 3679 -a password
# 切换数据库,默认为0
select 1
DBSIZE		# 返回当前数据库中 key 的数量
FLUSHDB		# 删除当前数据库的所有key
FLUSHALL	# 删除所有数据库的所有key
COMMAND COUNT		# 获取 Redis 命令总数
COMMAND				# 获取 Redis 命令详情
SAVE				# 同步保存数据到硬盘

# 注册服务 
redis-server --service-install redis.windows.conf
# 删除服务 
redis-server --service-uninstall
# 开启服务 
redis-server --service-start
# 停止服务 
redis-server --service-stop

配置文件

Redis 修改服务器配置的方式有2种:

  1. 使用 config set 命令的方式修改配置;
  2. 修改Redis的配置文件 redis.conf

注意:

​ 命令方式会立即生效,但下次服务重启会失效;而修改配置文件不会立即生效,必须服务器重启后才能生效;

实际使用中两者可以相结合使用。

redis.conf 配置文件

官方配置文件参考:http://download.redis.io/redis-stable/redis.conf

# 修改为守护进程模式; Redis默认不是以守护进程的方式运行,使用yes启用守护进程
daemonize yes
# 绑定的主机地址
bind 127.0.0.1
# 端口号
port 6379
# 当客户端闲置多长时间断开连接;如果配置0则关闭该功能
timeout 300
# 指定在多长时间内,有多少次变化,才将数据同步到磁盘,可以多个条件配合
save <seconds> <changes>

save 900 1			# 900秒(15分钟)内有1次写操作,数据同步到磁盘
save 300 10			# 300秒(5分钟)内有10次写操作,数据同步到磁盘
save 60  10000		# 60秒内有10000次写操作,数据同步到磁盘
# 指定RDB数据库文件的名称
dbfilename dump.rdb
# 指定RDB文件的存放目录
dir ./
# 数据持久化到RDB文件是否压缩数据,默认yes;如果为了节省CPU时间可以关闭,但会导致RDB数据库文件变大;
rdbcompression yes
# 是否每次写操作就进行日志记录(异步),默认no; 如果突然宕机,Redis有可能丢失一部分数据;
appendonly no
# 指定更新日志条件,有3个可选值
appendfsync everysec

no			# 表示等待操作系统将数据同步到磁盘(快)
always		# 每次写操作都会调用fsync将数据同步到磁盘(慢,影响性能)
everysec	# 表示每秒同步一次;(折中,默认值)
# 指定日志文件名称,默认appendonly.aof
appendfilename appendonly.aof
# 设置当前主机为slave时,设置master的ip和port; Redis启动时,自动从master进行数据同步;
slaveof <masterip> <masterport>
# 当 master 服务设置了密码保护时,slav 服务连接 master 的密码
masterauth <master-password>
# 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH <password> 命令提供密码,默认关闭
requirepass foobared
# 设置 Redis 最大内存限制;达到最大内存后,数据将无法进行写操作,但可以进行读操作;
maxmemory <bytes>
daemonize yes、no yes表示启用守护进程,默认是no即不以守护进程方式运行。其中Windows系统下不支持启用守护进程方式运行
port 指定 Redis 监听端口,默认端口为 6379
bind 绑定的主机地址,如果需要设置远程访问则直接将这个属性备注下或者改为bind * 即可,这个属性和下面的protected-mode控制了是否可以远程访问
protected-mode yes 、no 保护模式,该模式控制外部网是否可以连接redis服务,默认是yes,所以默认我们外网是无法访问的,如需外网连接rendis服务则需要将此属性改为no
loglevel debug、verbose、notice、warning 日志级别,默认为 notice
databases 16 设置数据库的数量,默认的数据库是0。整个通过客户端工具可以看得到
rdbcompression yes、no 指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变得巨大
dbfilename dump.rdb 指定本地数据库文件名,默认值为 dump.rdb
dir 指定本地数据库存放目录
requirepass 设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH命令提供密码,默认关闭
maxclients 0 设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息
maxmemory XXX 指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区。配置项值范围列里XXX为数值

配置命令

使用 CONFIG SET parameter valueCONFIG GET parameter 命令操作配置参数:

# 获取某参数的配置信息
127.0.0.1:6379> config get port	
1) "port"
2) "6379"

注:命令方式修改 redis 配置参数,无需重启就会生效,但服务器重启会失效,配合配置文件一起使用。

慢查询分析

​ 许多存储系统(例如MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阈值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis 也提供了类似的功能。

对于慢查询功能,需要明确 3 件事:

1、预设阈值怎么设置?

​ 在 redis 配置文件中修改配置 ‘slowlog-log-slower-than’ 的值,单位是微妙(1秒 = 1000毫秒 = 1000000微秒),默认是 10000 微秒,如果把 slowlog-log-slower-than 设置为 0,将会记录所有命令到日志中。如果把 slowlog-log-slower-than 设置小于0,将会不记录任何命令到日志中。

2、慢查询记录存放在哪?

​ 在 redis 配置文件中修改配置 ‘slowlog-max-len’ 的值。slowlog-max-len 的作用是指定慢查询日志最多存储的条数。实际上,Redis 使用了一个列表存放慢查询日志,slowlog-max-len 就是这个列表的最大长度。当一个新的命令满足满足慢查询条件时,被插入这个列表中。当慢查询日志列表已经达到最大长度时,最早插入的那条命令将被从列表中移出。比如,slowlog-max-len 被设置为 10,当有第11条命令插入时,在列表中的第1条命令先被移出,然后再把第11条命令放入列表。

​ 记录慢查询指 Redis 会对长命令进行截断,不会大量占用大量内存。在实际的生产环境中,为了减缓慢查询被移出的可能和更方便地定位慢查询,建议将慢查询日志的长度调整的大一些。比如可以设置为 1000 以上。

除了去配置文件中修改,也可以通过 config set 命令动态修改配置

> set slowlog-log-slower-than 1000
> config set slowlog-max-len 1200
> config rewrite

3、如何获取慢查询日志?

可以使用 slowlog get 命令获取慢查询日志,在 slowlog get 后面还可以加一个数字,用于指定获取慢查询日志的条数,比如,获取2条慢查询日志:

> slowlog get 3
1) 1) (integer) 6107
   2) (integer) 1616398930
   3) (integer) 3109
   4) 1) "config"
      2) "rewrite"
2) 1) (integer) 6106
   2) (integer) 1613701788
   3) (integer) 36004
   4) 1) "flushall"

可以看出每一条慢查询日志都有4个属性组成:

  1. 唯一标识ID
  2. 命令执行的时间戳
  3. 命令执行时长
  4. 执行的命名和参数

此外,可以通过 slowlog len 命令获取慢查询日志的长度;通过 slowlog reset 命令清理慢查询日志。

README

作者:银法王

版权声明:本文遵循知识共享许可协议3.0(CC 协议): 署名-非商业性使用-相同方式共享 (by-nc-sa)

参考:

Redis 官网命令参考手册

Redis 官方文档

Redis 教程【菜鸟教程】

万字好文!带你入门 redis【腾讯云开发者】

一文读懂 Redis 架构演化之路【腾讯云开发者】

一文搞懂redis【阿里开发者】

聊聊分布式锁【字节跳动技术团队】

Redisson 官方中文文档

Redis 实现分布式锁

Redis Redlock 的争论

Redis的五种数据结构的内部编码

修改记录:

 2020-02-26 第一次修订

 2020-03-19 完善内容

 2021-02-27 完善内容 

 2023-02-22 重构目录结构及内容


KV存储之Redis完全指北
http://jackpot-lang.online/2020/02/26/数据库/KV存储之Redis完全指北/
作者
Jackpot
发布于
2020年2月26日
许可协议