dcddc

西米大人的博客

0%

redis实践

安装

登录官网,redis.io,找到 mac 方式安装。推荐 Homebrew

安装后的 redis 路径:/opt/homebrew/Cellar/redis/7.0.5
配置文件路径:/opt/homebrew/etc/redis.conf

启动 redis 服务:redis-server
登录 redis 客户端命令行:redis-cli

  • 关闭 redis:shutdown

命令

String

  • set K V
  • get K V
  • mset K1 V1 K2 V2
  • mget K1 K2
  • getset K V
    • 先 get 旧值,然后 set
  • setnx K V
    • 不存在 K,才执行 set
  • STRLEN K
    • 获取 K 对应 V 的字符串长度
  • append K V
    • K 对应的 Value 后面增加字符串,没有 Value 则新增的字符串作为 Value
  • incr K
    • K 的 value+1,如果之前没有 set,K 的 value 执行后为 1
  • incrby K V
    • K 的 value+V,value 必须为数字,V 可以任意
  • decr K
  • decrby K V

List

存储的 value 类型是一个 List。Redis 中的 List 数据结构本质是通过双向链表实现的

  • lpush K V1 V2 V3
    • 从左侧 push,因此实际存储的顺序为 V3 V2 V1
  • rpush K V1 V2 V3
  • lpop K {count}
    • 从左侧 pop,count 表示 pop 的个数
  • rpop K {count}
  • blpop K {timeoutSeconds}
    • 如果 pop 失败会一直阻塞指定秒数,直到 pop 成功
  • brpop K {timeoutSeconds}
  • rpoplpush K K1
    • 从 K 列表的右侧 pop,左侧 push 到 K1 列表
  • brpoplpush K K1 {timeoutSeconds}
  • lrange K {start} {end}
    • 获取指定范围内的元素。start=0 表示起始位置,end=-1 表示终止位置
  • llen K
    • K 列表的长度
  • lindex K {index}
    • 获取指定索引上的元素
  • linsert K before|after {element} {newElement}
    • 在指定元素前或后插入新元素
  • lset K {index} {newElement}
    • 指定索引处设置新元素
  • ltrim K {start} {end}
    • 裁剪列表,只保留 start 下标到 end 下标区间的元素
  • lrem K {count} {element}
    • 从左侧开始删除指定个数的某个元素

set

存储的 value 类型是一个 Set。redis 的 set 数据类型底层基于 HashSet

  • sadd K V1 V2 V3
  • smembers K
    • 获取集合的所有元素
  • srem K {element}
    • 删除指定元素
  • smove K1 K2 {element}
    • 把元素从 K1 集合移动到 K2 集合
  • scard K
    • 获取集合中的元素数量
  • sismember K {element}
    • 判断元素是否在集合里,返回 1 表示存在,0 表示不存在
  • srandmember K
    • 从集合里随机获取一个元素
  • spop K {count}
    • 随机从集合中移除指定数量的元素
  • sinter K1 K2
    • 返回 K1 和 K2 的交集
  • sunion K1 K2
    • 返回 K1 和 K2 的并集
  • sdiff K1 K2
    • 返回只在 K1 里的元素
  • sinterstore K3 K1 K2
    • 集合 K1 和 K2 的交集存储到 K3
  • sunionstore K3 K1 K2
  • sdiffstore K3 K1 K2

zset

存储的 value 类型是一个 zet。zset 简单理解就是通过权重实现排序的集合

  • zadd K {score1} V1 {score2} V2
    • 添加带权重的元素
  • zrem K {element}
  • zrange K {start} {end} withscores
    • 获取排序后的指定下标区间内的集合元素,排序默认按 score 升序
  • zrevrange K {start} {end}
    • 获取逆序排序后的指定下标区间内的集合元素
  • zrank K {element}
    • 获取元素排序后的下标,默认升序
  • zrevrank K {element}
  • zrangebyscore K {minScore} {maxScore}
    • 获取权重范围内排序的所有元素
  • zrangebylex K [a (z
    • 返回在首字母区间内排序的所有元素,”[“表示包含,”(“表示不包含。
  • zrevrangebyscore K {maxScore} {minScore}
  • zcount K {minScore} {maxScore}
  • zlexcount K [a (z
  • zremrangebyrank K {start} {end}
    • 从集合移除开始到结束下标范围内的元素
  • zremrangebyscore K {minScore} {maxScore}
    • 从集合移除权重范围内的元素
  • zremrangebylex K [a (z
  • zunionstore K3 {zsetCounts} {zset1} {zset2} …
    • 取并集把结果存储到新的 zset,相同元素的权重会相加
  • zinterstore K3 {zsetCounts} {zset1} {zset2} …
    • 取交集把结果存储到新的 zset,相同元素的权重会相加
  • zpopmax K
    • pop 权重最大的元素
  • zpopmin K

Hash

存储的 value 类型是一个 hashMap。

  • hset K {key1} {value1} {key2} {value2} …
  • hget K {key1}
  • hmget K {key1} {key2} …
  • hsetnx K {key1} {value1}
  • hincrby K {key1} {incrment}
  • hstrlen K {key1}
  • hexists K {key1}
    • 返回 1 表示存在,0 表示不存在
  • hdel K {key1}
  • hlen K
  • hgetall K
  • hkeys K
  • hvals K

geo

经纬度相关操作

  • geoadd K {经度} {纬度} {member}
    • 添加地理位置,例如:geoadd china 115.7 39.4 beijing
  • geopos K {member}
    • 读取经纬度
  • geodist K {member1} {member2} {M/KM}
    • 计算两地的距离
  • georadiusbymember K {member} {distance} {M/KM}
    • 查询距离某地指定距离范围内的所有地点

bitmap

bitmap 本质也是 String 类型的数据结构,使用 bitmap 可以对 String 底层的比特位置 0 或 1。因此他有两个特定:非常省存储空间、适合大数据量二值统计
因为 redis 中 String 最大长度 512M,因此 bitmap 最多能存储 2^32 个 bit

  • setbit K {offset} {0/1}
  • getbit K {offset}
  • bitcount K
    • 统计 bit 为 1 的个数

持久化

rdb

rdb 持久化,指生成二进制 rdb 文件来存储内存中的 snapshot 数据

  • 优点:压缩效率高、数据恢复快
  • 缺点:无法做到实时(秒级)持久化、可读性差
    • redis 默认按一定持久化策略定期执行

查询 rdb 文件路径命令:config get dir

bgsave

执行 bgsave 命令会触发 rdb 持久化

  • fork 子进程,后台执行 snapshot 持久化,不阻塞 redis 主线程。持久化结束后子进程退出
    • 持久化过程:将当前时刻内存中的数据(snapshot 数据)压缩成二进制 rdb 文件,然后替代(如果存在)原 rdb 文件
  • 如果执行命令:flushdb 后再执行 bgsave,因为 flushdb 会清空内存 snapshot,所以持久化后的 rdb 文件没有数据

bgsave 使用 COW 的思想减少内存使用。COW(copy on write)是一种简单的读写分离思想,只有在写操作时才会复制一份数据,原数据继续接收读请求,在复制的空间写完后替代原内存空间(修改指针),在读多写少的场景可以显著提高并发度和内存利用率

  • 由此可知,COW 保证的是最终一致性而非强一致性

借鉴 COW 方式,fork 的子进程并没有自己的物理内存空间,而是和主进程共用一份内存空间。只有当主进程有写操作时,才会复制一份写操作对应的内存页到子进程,主进程可以继续处理写操作。所以极端情况下,bgsave 期间如果没有写操作,是不会发生内存复制的,能提高性能和内存利用率

  • 由此可知,bgsave 不会阻塞主线程,可以继续处理读写操作,但 bgsave 开始后新写入内存的增量数据不会被持久化到 rdb 文件

save

执行 save 命令也会触发 rdb 持久化,和 bgsave 的区别是他不会 fork 子进程,而是直接阻塞主线程直到持久化结束

redis.conf 配置文件里指定了 redis 默认的 rdb 持久化策略,满足策略会自动触发执行 save 命令:

  • After 3600 seconds (an hour) if at least 1 change was performed
  • After 300 seconds (5 minutes) if at least 100 changes were performed
  • After 60 seconds if at least 10000 changes were performed

如果修改这个策略,放开注释# save 3600 1 300 100 60 10000,并修改参数

上面的持久化策略针对的是 redis 运行态,在 redis 关闭、主从复制阶段也会持久化:

  • redis-server 端执行 shuntdown 阶段(CTRL+C 后),会将最后的 snapshot 持久化为 rdb 文件保存到磁盘
  • 主从复制阶段会生成 rdb 文件

RDB 文件的载入工作是在服务器启动时自动执行的,所以 Redis 并没有专门用于载入 RDB 文件的命令

aof

rdb 最大的问题是时效性差,宕机丢失的数据比较多,aof 提供了一种近实时的持久化方案

aof(append only file)会把客户端发送的命令追加(append)到 aof 文件,重启 redis 后,aof 文件里的命令会被重新执行一遍来重建数据

  • aof 默认是关闭的,改 redis.conf 的 appendonly,置为 yes 来开启 aof
  • 如果存在 aof 文件,会优先使用 aof 来重建数据,即优先级高于 rdb

aof 刷盘的频率由 redis.conf 的 appendfsync 值控制,redis 提供三种策略:

  • always,每次执行完客户端命令,都会执行刷盘操作。实时性最好,但效率不高
  • everysec,每隔 1 秒执行一次刷盘操作,1 秒内的命令都暂存在内核缓冲区。实时性和效率的折中,redis 默认使用这种策略
  • no,由操作系统控制刷盘时机,只有当内核缓冲区积累了一定的数据才执行。效率最高,实时性最差,宕机丢的数据最多

当启用 aof,关闭 redis 服务器时会强制执行一次刷盘

aof 会记录每条客户端命令,如果不加优化,aof 文件过大会对服务器造成影响,因此有了 aof 重写
所谓 aof 重写,指 redis 启动一个子进程,把当前时刻的内存数据库快照生成对应的 redis 命令,写到一个新的 aof 文件里

  • 如果 aof 重写期间有新的写指令,会双写到原 aof 文件和新 aof 文件的内核缓冲区,等 aof 重写完成了,再刷盘到新 aof 文件里,然后替代原来的 aof 文件,这就保证了重写 aof 文件依然有很高的时效性,且假设重写失败,原 aof 文件也不会丢指令

redis 会在满足一定条件时触发 aof 重写,该条件可以在 redis.conf 里配置,例如下面默认配置,当新写入的内容达到了 100%原 aof 文件大小,且 aof 文件已经达到 64mb,才会触发 aof 重写

  • auto-aof-rewrite-percentage 100
  • auto-aof-rewrite-min-size 64mb

aof 文件路径在配置文件里指定,根路径:

1
2
3
4
5
6
7
8
9
# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir /opt/homebrew/var/db/redis/

子路径和 aof 文件名:

1
2
3
4
5
6
7
appendfilename "appendonly.aof"

# For convenience, Redis stores all persistent append-only files in a dedicated
# directory. The name of the directory is determined by the appenddirname
# configuration parameter.

appenddirname "appendonlydir"

主从同步

redis 主从同步分为同步和命令传播两个阶段

同步

从服务器在初次复制或断线重连时发送 psync 同步请求给主服务器,主服务器会决定执行全量同步还是增量同步

执行全量同步的过程:
主服务器执行 bgsave 生成 rdb 快照,然后发送到从服务器。快照后面的命令会缓存到内核缓冲区,从服务器使用 rdb 重建数据后,主服务器将缓冲区的命令发送到从服务器,等从服务器执行完毕后(这期间主服务器短暂不执行写命令),主从数据完成同步,数据达到一致

可以看到全量同步非常消耗主服务器性能和 IO 资源,因此只会在下面这几种情况才会执行:

  • 请求的主服务器 id 并非当前服务器 id。这会发生在主服务器变更了或者从服务器初次连接时
  • 从服务器携带的 offset+1 对应的命令不在主服务器内核缓冲区中
    • 主服务器会用内核缓冲区保存最近的一些命令,缓冲区是一个先进先出的队列
    • offset 是命令在缓冲区里的字节位置下标,如果 offset+1 对应的指令不在缓冲区里,说明从服务器缺失的指令已经超过了缓冲区存储的,所以需要重新执行全量同步

如果 psync 请求的主服务器 id 为当前服务器,且 offset+1 在内核缓冲区中,执行增量同步,主服务器只会把从 offset+1 开始的命令发送给从服务器即可。从服务器收到并执行命令后,会保存命令最后一个字节的 offset,下次发起 psync 时传入

命令传播

主从完成全量同步后,从服务器会每秒给主服务器发送心跳包,携带从服务器已经同步到的最新命令字节 offset。主服务器如果发现内核缓冲区有大于该 offset 的命令,就把增量命令发给从服务器,即命令传播

哨兵机制

redis 使用哨兵机制检测主服务器和从服务器的在线状态,并在主服务器下线后选举一个从服务器作为新的主服务器

为了保证哨兵服务器的高可用,redis 也会组哨兵集群,每个哨兵都会通过发布订阅模式,向主服务器发布自己的 ip 和端口号(主服务器 ip 和端口在哨兵的 conf 配置文件里),并通过订阅拿到其他哨兵服务器的 ip 和端口号,建立和其他哨兵的连接

每个哨兵不断地用 ping 命令看主服务器有没有下线,如果主服务器在「配置时间」内没有正常响应,那当前哨兵就「主观」认为该主服务器下线了,它会给其他哨兵发送请求,让其他哨兵判断是否主服务器下线了。如果「足够多」(还是看配置)的哨兵认为该主服务器已经下线,那就认为「客观下线」,「哨兵」之间会选出一个「领头」,由「领头哨兵」重新选主,一般选从服务器中 offset 最大的(同步的数据最接近原主服务器)

重新选主不可避免的会造成数据丢失,原因有二:

  • 主从同步延时
  • 发生脑裂(主服务器没下线,还在继续提供服务,则从认为它下线到新的主服务器正式工作前的这段时间的数据会丢失)

单机搭建主从和哨兵集群

主从集群

复制多份 redis.conf,然后做这些修改:

  • 修改默认 6379 端口,每份 redis.confi 配置的端口号不同即可。目的是给每个 redis 服务器进程分配独立的端口号
1
2
3
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 9001
  • 修改绑定的 ip,默认绑定本机 127.0.0.1。如果想其他 ip 或者本机的其他进程能连上该 redis 服务器,需要把本机 ip 和其他要建连的 ip 加上
1
2
# bind 127.0.0.1 ::1
bind {ip1} {ip2}
  • redis.conf 默认开启保护模式:protected-mode yes,此时主服务器必须设置登录密码
    • 设置密码后,客户端登录需要-a 123456来指定密码
1
requirepass 123456
  • 从服务器的 redis.conf 配置主服务器的密码:masterauth 123456

  • 从服务器的 redis.conf 配置主服务器的 ip 和端口号:replicaof {ip} 9001

  • 修改 rdb 和 aof 文件名,便于区分

1
2
appendfilename "appendonly-9001.aof"
dbfilename dump-9001.rdb

登录客户端,输入命令info replication,可查看主从信息

哨兵集群

复制多份 redis-sentinel.conf,然后做这些修改:

  • 修改端口号,同上
  • daemonize yes,后台运行
  • pidfile /var/run/redis-sentinel-9101.pid,指定后台运行的 pid 文件名,不同哨兵节点的 pid 文件名不同
  • sentinel monitor mymaster {ip} {port} {认为主服务器断连的哨兵数目阈值,达到阈值就认为主服务器断连},主服务器 ip 和端口号
  • sentinel down-after-milliseconds mymaster 1000,主服务器断连时长达到该值认为断连,默认 30s,测试可以改为 1s
  • sentinel parallel-syncs mymaster 1,重新选主后,同时最多有多少个从服务器对主服务器进行数据同步,默认是 1,即串行同步
  • sentinel failover-timeout mymaster 5000,完成故障转移(从发现 master 挂了到新的 master 开始提供服务)的时间阈值,超过这个时间认为失败,默认 3 分钟,测试可以改为 5s
  • sentinel auth-pass mymaster 123456,设置访问主从服务器需要的密码,集群中主从服务器的密码需要设置成一样的

分片

为了解决单机内存容量限制,redis 采用哈希槽方式构建 redis 分片集群。哈希槽方式使得扩缩容对每个节点的数据影响相对较小,因为缩容时,原节点数据会均分到其他节点,扩容时,每个节点的一小部分数据迁移到新节点。如果使用一致性哈希算法,扩缩容对相邻节点的数据影响很大,不符合 redis 的高可用理念

在实际应用中哈希槽分片有两种具体实现

redis cluster

  • 将 16384 个哈希槽分配(默认均分)到所有 redis 主服务器
  • redis 主服务器之间互相通信,每个节点都知道哈希槽在整个 redis 集群的分布情况(无中心化设计)
  • 客户端路由:客户端本地也会存储哈希槽在 redis 集群的分布情况。每次发起请求,先对 key 做哈希,然后向持有该槽的 redis 服务器直接发起请求(客户端直连服务器)
  • 扩缩容时进行数据迁移,迁移期间如果收到操作受影响数据的命令,会返回新的节点地址给客户端。如果数据迁移完毕,还会让客户端更新本地路由
    • 迁移期间,原数据节点不执行命令,新节点要等数据迁移完后才执行命令,所以 redis cluster 模式是同步迁移的,即迁移期间会影响数据读写

codis

  • 将 1024 个哈希槽分配(默认均分)到所有 redis 主服务器
  • 使用 zk 维护节点和哈希槽的分布情况(中心化设计)
  • 服务端路由:codis proxy 本地缓存 zk 的节点路由数据,对客户端请求做路由。客户端连接的是 codis proxy
  • 扩缩容时支持数据异步迁移,即数据迁移过程中,原节点依然能处理受影响数据的读请求,但写请求会路由到新节点,且要等到迁移完成后才能执行

单机搭建 redis cluster

复制多份 redis.conf,并做如下配置:

  • # bind 127.0.0.1 ::1
  • protected-mode no
  • cluster-enabled yes
  • cluster-node-timeout 15000
  • daemonize yes

启动每个 redis.conf 对应的 redis 服务器实例:redis-server xxx.conf

创建 redis cluster 集群:redis-cli –cluster create –cluster-replicas 1 {ip:port1} {ip:port2} {ip:port3} {ip:port4} {ip:port5} {ip:port6}

  • 1 表示从节点个数
  • 根据节点(ip:port)个数和每个主节点的从节点个数来创建 cluster,例如上面的配置,会创建 3 个主节点,每个主节点各 1 个从节点
    • redis cluster 最低配置 6 个节点:3 个主节点和 3 个从节点
  • 如果创建失败,清空/opt/homebrew/var/db/redis目录下的所有数据,该目录存放 aof、rdb 文件

客户端按集群模式登录 redis 服务器时,需要增加-c
输入命令cluster info,可查看集群信息

springboot 连接 redis cluster

引入 redis 依赖和 test 组件

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

yml 配置 redis cluster 连接信息

1
2
3
4
5
6
7
spring:
redis:
cluster:
nodes: ip:port, ip:port, ... # 这里只要配置集群里每个主节点的ip和端口号
# host: localhost
# port: 6379
# database: 0

测试类:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest(classes = {App.class})
public class RedisTest {

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Test
void demo1() {
redisTemplate.opsForValue().set("hername", "jay");
}
}

执行测试用例后,可以观察 aof 的修改时间,就知道写操作路由到哪个节点了