Redis基础

布鸽不鸽 Lv4

前言

本文是Redis基础学习笔记

原文地址:https://xuedongyun.cn/post/26216/

Redis入门概述

是什么

能干什么

  • 挡在MySQL前面的带刀护卫

  • 内存存储和持久化(RDB+AOF)

    • redis支持异步将内存中的数据写到硬盘上,同时不影响继续服务
  • 高可用架构搭配

    • 单机
    • 主从
    • 哨兵
    • 集群
  • 缓存穿透,击穿,雪崩(预防和处理)

  • 分布式锁

  • 队列

  • 排行榜+点赞

  • ……

优势

  • 性能极高
  • 数据类型丰富,key-value,list,set,zset,hash
  • 支持数据的持久化
  • 支持数据的备份,即master-slave模式的数据备份

Redis7新特性

  • https://github.com/redis/redis/releases
  • 部分新特性总览
    • Redis Functions:相当于Redis内部的lua脚本(不用了解)
    • Client-eviction:连续内存占用独立管理
    • Multi-part AOF:三个文件构成一个AOF文件(以前是一个文件)
    • ACL v2:精细化权限管理
    • 新增命令
      • 新增ZMPOP,BZMPOP,LMPOP,BLMPOP等新命令
      • 对于EXPIRE和SET命令,新增了更多的命令参数选项
      • 需要用到再查文档即可
    • listpack替代ziplist
    • 底层性能提升

Redis7安装相关

  • 安装位置/usr/bin
    • redis-benchmark:性能测试工具
    • redis-check-aof:修复有问题的AOF文件
    • redis-check-rdb:修复有问题的dump.rdb文件
    • ==redis-cli==:客户端,操作入口
    • redis-sentinel:Redis集群使用
    • ==redis-server==:Redis服务器启动命令
  • 关闭
    • redis中:shutdown
    • 单实例关闭:redis-cli -a 123456 shutdown
    • 多实例,指定端口关闭:redis-cli -p 6379 shutdown

Redis十大数据类型

https://redis.io/docs/data-types/

总体概括

  • String
    • 最基本的类型
    • 是二进制安全的,意思是可以包含任何数据,比如图片或序列化的对象
    • 最大512M
  • List
    • String类型的列表
    • 是一个双端链表
    • 最多包含2^32-1个元素(约40亿)
  • Hash
    • String类型的field-value映射表
    • 特别适合存储对象
    • 最多包含2^32-1个键值对(约40亿)
  • Set
    • String类型的无序集合,成员是唯一的
    • 集合对象的编码可以是intsethashtable(后面会讲)
    • 底层是通过Hash实现的
    • 最多包含2^32-1个元素(约40亿)
  • Sorted set(zset)
    • 不同的是,每个元素都会关联一个double类型的分数,从小到大排序
  • Geospatial
    • 存储地理位置
    • 可以操作:添加地理位置的坐标,获取地理位置的坐标,计算位置间的距离,根据给定经纬度坐标获取指定范围内地理位置集合
  • Hyperlog
    • 基数统计
    • 输入元素非常大时,计算基数所需的空间总是固定且很小的
    • 每个HyperLogLog键只需要12KB,就可以计算2^64个不同元素的基数
    • 只会根据输入元素计算基数,不会存储元素本身
  • Bitmap
    • 由0,1状态表现的二进制数组
  • Bitfield
    • 可以一次性操作多个比特位域(连续多个比特位),会执行一系列操作并返回一个响应数组
  • Stream
    • 主要用于消息队列
    • 可实现redis版的消息中间件

命令查询

常用key操作命令

  • 查看当前库所有key
1
keys *
  • 判断某个key是否存在(返回存在个数)
1
2
exists k1
exists k1 k2 k3
  • 返回值类型
1
type k1
  • 删除,成功返回1
1
del k1
  • 非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除将在后续异步中进行
1
unlink key
  • 查看还有多少秒过期,-1表示永不过期,-2表示已过期
1
ttl key1
  • 为key设置过期时间
1
expire k1 100
  • 切换数据库,index(0-15)
1
select 0
  • 将当前数据库key移动到给定index(0-15)的数据库中
1
move key 2
  • 查看当前数据库key的数量
1
dbsize
  • 清空当前库
1
flushdb
  • 通杀全部库
1
flushall

数据类型命令及落地应用

  • 命令不区分大小写,key区分
  • 帮助命令
1
help @string

string字符串

  • 最常用

    • NX:键不存在时才设置键值

    • XX:键存在时才设置键值

    • GET:返回键原本的值,原本无值返回nil

    • EX seconds:以秒为单位设置过期时间

    • PX milliseconds:以毫秒为单位设置过期

    • EXAT unix-time-seconds:以秒为单位的unix时间戳对应时间为过期时间

    • PXAT unix-time-milliseconds:以毫秒为单位的unix时间戳对应时间为过期时间

    • KEEPTTL:保留设置前指定键的生存时间

1
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
  • 同时设置/获取多个值
1
2
MSET k1 v1 k2 v2
MGET k1 k2
  • 不存在键时才设置为指定值
1
SETNX key value
1
MSETNX k1 v1 k2 v2 // 类似事务,都之前不存在才能被设置成功
  • 设置/获取指定范围的值,左右都取
1
2
3
set k1 abcdefg
getrange k1 0 -1 // abcdefg
getrange k1 0 4 // abcde
  • 精准设置字符串值
1
setrange k1 1 xxyy // 从1开始变为“xxyy”
  • 数值递增/递减
1
2
3
4
incr k1
incr k1 99
decr k1
decr k1 99
  • 获取字符串长度和内容追加
1
2
strlen k1
append k1 abcd
  • 分布式锁
1
2
setnx key value
setex key value

setnx(set if not exist)

setex(set with expire)

1
setex key seconds value // set的同时设置过期时间

高阶篇再具体讲

1
2
3
setnx lock uuid
xxxx
del lock
  • getset,先get再set;最后返回oldValue
1
getset k1 newValue

list列表

底层是一个双端链表

  • 入队以及遍历
1
2
3
lpush list1 1 2 3 4 // 返回修改数
rpush list2 1 2 3 4
lrange list start end // 一定范围内遍历,没有rrange
  • 出队
1
2
lpop list1
rpop list1
  • 根据索引取值
1
lindex list1 0 // 没有rindex
  • 从左边开始,删除最多num个值为value的项
1
lrem key num value
  • 截取指定范围的值,代替原有值
1
ltrim key start end
  • 源列表右侧出队,目标列表左侧入队
1
rpoplpush source destination // 返回修改的那个值
  • 设置对应索引的值
1
lset key index value
  • 在已有值(从左开始找到的第一个)前/后插入新值
1
linsert key before/after 已有值 新值

hash哈希

Map<String, Map<String, Object>>

  • 基本操作
1
2
3
4
5
hset key field1 value1 field2 value2 // 返回修改field的数量
hget key field
hmget key field1 field2
hgetall key
hdel key field1 field2

hmset已被弃用,使用hset代替

  • 获取某个key,field的数量
1
hlen key
  • 某个key是否存在某个field,返回0或1
1
hexists key field
  • 获取某个key中所有field/value
1
2
hkeys k1
hvals k1
  • 值增加
1
2
hincrby key field value // 整数,返回修改后的值
hincrbyfloat key field value // 小数,返回修改后的值
  • 不存在才赋值
1
hsetnx key field value // 返回0或1

应用场景:购物车(现在不这样做了,中小厂可以这样做)

  • 新增商品
1
2
hset shopcar:uid1024 334488 1
hset shopcar:uid1024 334477 1
  • 增加商品数量
1
hincrby shopcar:uid1024 334488 1
  • 商品总数
1
hlen shopcar:uuid1024
  • 全选
1
hgetall shopcar:uuid1024

set集合

  • 添加元素
1
sadd key member1 member2
  • 删除元素
1
srem key member1 member2
  • 遍历所有元素
1
smenbers key
  • 判断元素是否在集合中
1
sismember key member
  • 统计元素数量
1
scard key
  • 从集合中随机展现指定个数个元素
1
srandmember key 5
  • 从集合中随机弹出指定个数个元素,元素会被删除
1
spop key 5
  • 将一个元素从一个set移动到另外一个set,返回0或1
1
smove source destination member
  • 集合运算
1
2
3
4
5
6
7
8
9
10
11
// 差集运算:A - B - C
sdiff key1 key2 key3

// 并集运算:A ∪ B ∪ C
sunion key1 key2 key3

// 交集运算:A ∩ B ∩ C
sinter key1 key2 key3
sintercard numkeys key [key ...] // redis7.0新出的,返回交集运算结果的基数
sintercard 2 set1 set2
sintercard 2 set1 set2 limit 4 // 可以设置上限

应用场景:

微信抽奖小程序:

1
2
3
4
sadd key userId		// 立即参与
scard key // 统计参与人次
srandmember key 2 // 随机抽奖2个人,元素不删除(中奖可重复)
spop key 2 // 随机抽奖2个人,元素删除(中奖不可重复)

朋友圈点赞

1
2
3
4
5
sadd pub:msgId userId1 userId2	// 新增点赞
srem pub:msgId userId1 // 取消点赞
smembers pub:msgId // 展现所有点赞用户
scard pub:msgId // 点赞数
sismember pub:msgId userId1 // 判断是否点赞过

qq可能认识的人

1
2
3
4
5
sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7
sinter s1 s2 // 共同认识
sdiff s1 s2 // s2可能认识
sdiff s2 s1 // s1可能认识

zset有序集合

在set基础上,每个val值前加上一个score分数

  • 基础
1
2
set v1 v2 
zset score1 v1 score2 v2
  • 添加元素
1
zadd zset1 60 v1 70 v2 30 v3
  • 遍历元素
1
2
3
// 元素排序从小到大
zrange zset1 0 -1
zrange zset1 0 -1 withscores // 带分数的遍历
1
2
3
// 元素排序从大到小
zrevrange zset1 0 -1
zrevrange zset1 0 -1 withscores // 带分数的遍历
1
2
3
4
zrangebyscore key 60 90		// 指定分数范围内
zrangebyscore key ( 60 90 // 不包含60
zrangebyscore key 60 ( 90 // 不包含90
zrangebyscore key 60 90 limit 0 6 // 类似分页,从0开始6个
  • 查询score
1
zscore key member	// 返回分值
  • 获取元素个数
1
zcard key
1
zcount key min max // 指定分数范围元素个数
  • 删除元素
1
zrem key member1 member2 // 返回删除个数
  • 增加分数
1
zincrby key 3 member // 增加某个元素的分数
  • 弹出元素
1
2
3
ZMPOP numkeys key [key ...] <MIN | MAX> [COUNT count]
// 从小到大or从大到小,弹出指定数量的元素
// count负责控制弹出多少,不写默认为1
  • 获取下标值
1
2
zrank key value		// 从小到大
zrevrank key value // 从大到小

应用场景:

根据商品销量进行排序

1
2
3
4
zadd goods:sellsort 9 1001 15 1002
zrange goods:sellsort 0 10 with scores // 销量前十
zincrby goods:sellsort 2 1001 // 1001商品销量加2
zrange goods:sellsort 0 10 with scores // 销量前十

bitmap位图

由0和1状态表现的二进制数组

底层基于string类型,按位操作

  • 设置
1
2
3
4
5
6
7
setbit key offset value // value只能是0或1

setbit k1 0 1
setbit k1 1 1
setbit k1 2 1
setbit k1 3 0
type k1 // string
  • 获取值
1
2
3
getbit k1 0
getbit k1 1
getbit k1 2
  • strlen
1
2
3
4
5
6
// 底层是string,超过8位后,按照8位一组(1 byte)扩容
setbit k2 0 1
setbit k2 7 1
strlen k2 // 1
setbit k2 8 1
strlen k2 // 2
  • bitcount
1
bitcount key	// 全部1的个数
  • 进行位操作(and or xor not)
1
2
// 最终结果将放到destkey中
BITOP <AND | OR | XOR | NOT> destkey key [key ...] // 返回0或1指示是否成功

应用场景:签到

1
2
3
4
5
6
7
setbit sign:u1:202106 0 1	// 某用户21年6月,第一天签到了
setbit sign:u1:202106 1 1
setbit sign:u1:202106 2 1
setbit sign:u1:202106 29 1

getbit sign:u1:202106 2 // 第二天来没来
bitcount sign:u1:202106 // 21年6月来了多少天

hyperloglog基数统计

用来做基数统计的,在输入元素的数量或体积特别大时,计算基数所需的空间总是固定且很小的

每个HyperLogLog键只需要花费12KB,就可以计算2^64个不同元素的基数(标准误差0.81%)

底层类型string

  • 基数:一种数据集,去重后的真实个数
  • 添加指定元素到HyperLogLog
1
pfadd key element1 element2
  • 返回给定HyperLogLog的基数估算值
1
pfcount key
  • 将多个HyperLogLog合并为同一个
1
pfmerge destkey sourcekey1 sourcekey2 sourcekey3

geo地理空间

底层zset类型

  • 添加经纬度坐标
    • 解决中文乱码问题:redis-cli -a 111111 –raw
1
geoadd city 116.403963 39.915119 天安门 116.403414 39.924091 故宫 116.024067 40.362639 长城 // 返回添加的个数
  • 返回经纬度
1
geopos key member1 member2 member3
  • 返回坐标的geohash表示
    • geohash算法生成的base32编码值
1
geohash key member1 member2 member3
  • 返回距离
1
geodist key member1 member2 [m|km|ft|mi]
  • 查找指定半径中的位置
    • withdist:返回距离
    • withcoord:返回坐标
    • withhash:返回位置经过geohash编码后的值
    • count:限定返回记录数
    • asc/desc:由近到远/由远到近
    • store/storedist:将结果保存到zset,前者score是geohash值,后者是距离
1
2
3
GEORADIUS key longitude latitude radius <M | KM | FT | MI>
[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC]
[STORE key] [STOREDIST key]

stream流

就是redis版本的消息中间件(MQ)+阻塞队列

  • list实现消息队列:
    • 用来做异步队列
    • 点对点模式
1
2
3
lpush mylist a b c d e
rpop mylist
rpop mylist
  • pub/sub发布和订阅机制实现消息队列:
    • 一对多模式
    • 缺点:消息无法持久化,如果网络断开,宕机等,消息就会丢失;而且也没有ACK机制保证数据可靠性
  • 能干什么

    • 实现消息队列
    • 支持消息持久化
    • 支持自动生成全局唯一id
    • 支持ack确定消息的模式
    • 支持消费组模式
    • …让消息队列更加稳定可靠
  • 底层结构

    • 一个消息链表,每个消息都有唯一id和相应内容
名称内涵
Message Content消息内容
Consumer group消费组,通过xgroup create命令创建,同一个消费组可以有多个消费者
Last_delivered_id游标,每个消费组都有一个,任意消费者读取了消息都会使游标往前移
Consumer消费者,消费组中的消费者
Pending_ids消费者会有一个状态变量,用于记录已读取未ack的消息id。
如果客户端没有ack,这个变量中的id会越来越多;一旦某个消息被ack,它就开始减少。
这个变量Redis官方称之为PEL(Pending),记录当前已被客户端读取,但还没有ack的消息。
它用来确保客户端至少消费了消息一次,而不会在网络传输的过程中丢失了没处理
队列相关指令
  • 添加消息到队列末尾
    • 消息id必须比上一个大
    • *表示自动生成规矩(让系统自动生成id)
1
xadd mystream * k1 v1 k2 v2 // 返回消息id:"1678332037505-0",该毫秒下第0条消息

如果当前毫秒时间戳比以前已存在的小,系统会采用以前相同的毫秒创建新的id(自动纠错)

redis对id有强制要求,格式必须是==时间戳-自增id==,且后续id不能小于前一个

  • 遍历所有消息
1
2
3
xrange key start end [count num] // -表示开始值,+表示结束值
xrange key - + count 2
xrevrange key + - count 2
  • 删除消息
1
xdel key id
  • 获取消息队列长度
1
xlen key
  • 对stream长度进行截取,如超长会截取
1
2
3
// maxlen: 允许的最大长度
// minid:允许的最小id
xtrim key maxlen 2 minid 1678332037505-0
  • 读取消息
1
2
3
// COUNT: 最多读取多少条信息
// BLOCK: 是否堵塞的方式读取消息,默认不阻塞;如果miliseconds设为0,表示永远堵塞
xread [COUNT count] [BLOCK miliseconds] streams key1 key2 key3
1
2
xread count 2 streams mystream $	// $是特殊id,表示stream已经存储的最大id的后一个id。此处返回nil
xread count 2 streams mystream 0-0 // 0-0代表从最小id开始获取stream中的消息,也可使用0;00;000
1
xread count 1 block 0 streams mystream $	// 阻塞等待,一直等到出现新的id。还会返回等待时间
消费组相关指令
  • 创建消费组
1
2
xgroup create key groupA $	// $表示从尾开始消费(只消费新的)
xgroup create key groupB 0 // 0表示从头开始消费
  • 消费组读取信息
1
2
3
4
5
6
7
xreadgroup group groupB consumer1 streams mystream >	// 能读到消息
xreadgroup group groupB consumer2 streams mystream > // nil,消息一旦被消费组中一个消费者读取了,就不能再次被组内其他消费者读取了

// 组内多个消费者分担读取,实现读取的负载均衡
xreadgroup group groupC consumer1 count 1 streams mystream >
xreadgroup group groupC consumer2 count 1 streams mystream >
xreadgroup group groupC consumer3 count 1 streams mystream >

消息一旦被消费组中一个消费者读取了,就不能再次被组内其他消费者读取了

不同消费组之间不受影响

  • 查询消费组内,所有消费者“已读取,但尚未确认”的消息
1
xpending mystream groupA
1
2
3
4
5
6
// 返回值:总条数;起始时间戳;消费者极其消费的条数
1) (integer) 4
2) "1678332037505-0"
3) "1678347842560-0"
4) 1) 1) "constomer1"
2) "4"
1
xpending mystream groupB - + 10 consumer2
  • 签收消息
1
xack mystream groupB 1678332037505-0
  • 打印Stream/Consumer/Group的详细信息
1
xinfo stream mystream
graph LR;
    Publisher--"xadd"-->Stream
    Stream-->Consumer1
    Stream--"xgroupcreate"-->Consumer2
    Stream-->Consumer3
    Consumer2--"xreadgroup"-->Client
    Client--"xack"-->Consumer2

bitfield位域

  • 了解即可

Redis持久化

  • redis持久化方式

    • RDB (Redis Database)

    • AOF (Append Only File)

    • RDB + AOF

RDB方式

  • 以指定的时间间隔,执行数据集的时间点快照

    • 保存备份时是全量快照
    • dump.rdb
  • 保存时间间隔,配置文件(6 vs 7)

    • 以前
    1
    2
    3
    save 900 1 # 900s,1次修改
    save 300 10
    save 60 10000
    • 现在
    1
    save 3600 1 300 100 60 10000

配置说明

1
/etc/redis/redis.conf
  • 本次案例5秒2次修改
1
2
3
4
5
6
7
8
9
# Unless specified otherwise, by default Redis will save the DB:
# * 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
#
# You can set these explicitly by uncommenting the following line.
#
# save 3600 1 300 100 60 10000
save 5 2
  • 修改dump文件保存位置
1
2
3
4
5
6
7
8
9
10
# 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 /var/lib/redis
dir /home/xdy/myredis/dumpfiles
  • 修改默认文件名
1
2
3
# The filename where to dump the DB
# dbfilename dump.rdb
dbfilename dump6379.rdb
  • 开启redis
1
2
3
4
5
6
7
redis-server ./redis-my.config
redis-cli

// 获取配置,查看修改成功与否
config get save
config get dir
config get dbfilename

自动触发

  • 依照save所设置的那样
  • 恢复方式
    • 将备份的dump.rdb移动到设置的目录中即可

手动触发

  • save

    • 会阻塞当前redis服务器,期间redis不能处理其他命令
    • 线上禁止使用!
  • bgsave

    • 后台异步进行快照操作
    • 如何工作的
      • redis forks,此时我们有一个子进程一个父进程
      • 子进程开始将数组从内存写到硬盘
      • 子进程完成后,会替换旧的进程
  • lastsave

    • 获取最后一次成功执行快照的时间

RDB优缺点

  • 优势
    • 适合大规模数据恢复
    • 按照业务定时备份
    • 对数据完整性和一致性要求不高
    • RDB文件在内存中加载速度比AOF快得多
  • 劣势
    • 如果redis意外宕掉,快照之后的数据会丢失
    • 数据量太大会导致I/O,严重影响性能
    • RDB依赖于主进程的fork,可能导致请求的瞬间延迟。fork时内存数据被克隆,有2倍的膨胀

RDB修复命令简介

1
redis-check-rdb dump6379.rdb

RDB触发小结和快照禁用

  • 触发

    • 配置文件中默认的配置
    • 手动save/bgsave
    • 执行flushall/flushdb,但里面是空的意义不大
    • 执行shutdown,且没有开启AOF持久化
    • 主从复制时,主节点主动触发
  • 如何禁用快照

    • 动态停止保存规则
    1
    redis-cli config set save ""
    • 快照禁用
    1
    2
    3
    4
    # Snapshotting can be completely disabled with a single empty string argument
    # as in following example:
    #
    save ""

RDB优化配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
save <seconds> <change>
dbfilename
dir

# 快照写入失败时,redis是否继续接收新的写请求
stop-writes-on-bgsave-error yes

# 存储到磁盘的快照是否压缩
rdbcompression yes

# 存储快照后,是否进行数据校验
rdbchecksum yes

# Redis主从全量同步时,通过RDB文件传输实现。
# 如果没有开启持久化,同步完成后,是否要移除主从同步的RDB文件,默认为no。
rdb-del-sync-files no

AOF方式

  • 是什么

    • 以日志的形式来记录每个写操作
    • redis启动之初,会读取该文件重新构建数据
  • 开启方式

    1
    2
    # 配置文件中默认未开启
    appendonly yes

AOF工作流程和写回操作

  • 工作流程

    • Client作为命令来源,会有多个源头,请求命令源源不断

    • 这些命令先放入AOF缓存中进行保存

    • AOF缓存会根据AOF缓冲区,==同步文件的三种写回策略==,将命令写入磁盘上的AOF文件

    • 随着写入AOF内容的增加,会==根据规则进行命令的合并==(又称AOF重写)

  • 三种写回策略

    • always:每个命令执行完,立刻同步将日志写回磁盘
    • everysec:先把日志写到AOF文件的内存缓冲区,每隔一秒将缓冲区内容写回磁盘
    • no:先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
    1
    2
    # 配置文件,默认everysec
    appendfsync everysec

    总结:

    配置项写回时机优点缺点
    always同步写回可靠性高,数据基本不丢失每个命令都要落盘,性能影响大
    everysec每秒写回性能适中宕机时丢失1秒内数据
    no操作系统控制的写回性能好宕机时丢失数据较多

AOF功能配置开启

  • 配置开启
1
2
3
4
5
# 配置文件中默认未开启
appendonly yes

# 使用默认写回策略
appendfsync everysec
  • AOF文件保存路径,AOF文件保存名称
1
2
3
4
5
6
7
# redis6之前

dir /myredis
appendfilename "appendonly.aof"

# RDB: /myredis/dump.rdb
# AOF: /myredis/appendonly.aof
1
2
3
4
5
6
7
8
9
10
# redis7

dir /myredis
appendfilename "appendonly.aof"
appenddirname "appendonlydir"

# RDB: /myredis/dump.rdb
# AOF: /myredis/appendonlydir/appendonly.aof.1.base.rdb
# /myredis/appendonlydir/appendonly.aof.1.incr.aof, appendonly.aof.2.incr.aof, ...
# /myredis/appendonlydir/appendonly.aof.manifest

base: 基础AOF,由子进程通过重写产生,该文件最多一个

incr: 增量AOF,后续写操作记录其中,该文件可能存在多个

history: 历史AOF,由base和incr变化而来,每次AOFRW成功完成,本次之前对应的base和incr都将变为history。==history类型的aof会被redis自动删除==

manifest: 清单文件,来跟踪管理这些AOF文件

AOF正常恢复

  • 写操作,自动生成AOF文件到指定目录
  • 恢复,重启redis时自动加载

AOF异常恢复

  • 可能:内容写了一小半,没有写完整,突然redis挂了,导致AOF文件错误
  • 往appendonly.aof.1.incr.aof最后一行胡乱插入一行,模拟文件损坏的情况
  • AOF文件有误,发现无法启动redis,连接直接报错
1
2
3
4
# 异常修复指令
# redis-check-aof --fix <file.manifest|file.aof>

redis-check-aof --fix ./appendonly.aof.1.incr.aof

AOF优缺点

  • 优点:
    • 更好保护数据不丢失,性能高,可做紧急恢复
  • 缺点
    • AOF文件远大于RDB文件,恢复速度慢于RDB
    • 根据不同的同步策略,AOF一般都比RDB慢。everysec的情况下,性能消耗任然很高。即使no的情况下,也和RDB差不多快。

AOF重写机制

  • 自动触发

    • 是上次rewrite后大小的一倍,且文件大于64M
    1
    2
    3
    4
    5
    6
    # 官方默认配置
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb

    # 是否同时开启rdb和aof。默认是yes,这里为了实验方便,设为no
    aof-use-rdb-preamble no
  • 手动触发

    1
    bgrewriteaof

AOF文件重写并不是对原文件重新整理,而是直接读取服务器现有键值对,生成新的AOF文件代替原来AOF文件

AOF重写原理

  1. redis fork一个子进程。子进程基于当前内存中的数据,构建日志,往新的临时AOF文件中写入日志
  2. 开启AOF重写缓冲区。保存这段时间内新增的写指令
  3. 主进程继续写旧文件。写入AOF重写缓冲区,进而继续写入旧AOF文件
  4. AOF重写缓冲区的指令追加到新文件
  5. 新AOF文件替代旧AOF文件

RDB-AOF混合持久化

1
2
# 混合模式默认开启
aof-use-rdb-preamble yes

数据恢复流程和加载顺序

  • 默认开启RDB
  • 如果开启AOF,优先加载AOF
graph TB;
    A[加载]-->B{存在AOF?}
    B-->|yes|C[加载AOF文件]
    B-->|no|D{存在RDB}
    D-->|yes|E[加载RDB文件]
    C-->F[启动成功or失败]
    E-->F
    D-->|no|F

使用选择

  • AOF保存更完整,因此优先使用AOF恢复
  • RDB适合用于备份数据库(AOF不断变化不好备份)
  • 推荐:RDB+AOF混合使用

纯缓存模式

  • 同时关闭RDB+AOF
1
2
3
4
5
# 禁用rdb持久化模式下,仍可以使用save,bgsave命令生成rdb文件
save ""

# 禁用AOF功能,仍可以使用bgrewriteaof生成aof文件
appendonly no

Redis事务

  • 依次执行多个命令,本质就是一组命令的集合
  • 一个事务中所有命令都会序列化,按顺序串行执行,不会被其他命令加塞

Redis事务vs数据库事务

  1. 单独的隔离操作
    • redis事务仅仅保证事务中操作会被连续独占的执行。(redis命令执行是单线程架构)
  2. 没有隔离级别的概念
    • 事务提交前任何指令都不会被实际执行
  3. 不保证原子性
    • redis不保证所有指令同时成功或失败,只有决定是否开始执行全部指令的能力,没有回滚的能力
  4. 排他性
    • redis会保证一个事务内的命令依次执行,不被其他命令插入

使用案例

  • 正常执行
1
2
3
4
multi
set k1 v1
expire k1 30
exec
  • 放弃执行
1
2
3
multi
set k1 v1
discard
  • 全体连坐
    • 在exec执行之前,有命令出错,全体无法执行
1
2
3
multi
set k1 # 发生错误
exec # Redis直接返回错误,所有命令都不会执行
  • 冤有头债有主
    • 在exec执行之后出错,其他命令不受影响(redis没有rollbask功能)
1
2
3
4
multi
set email 1234@qq.com
incr email # 运行时错误
exec # 其他命令不受影响
  • watch监控
    • Redis使用watch来提供乐观锁,类似于CAS(Check-and-Set)
1
2
3
4
5
6
7
set balance 100
watch balance # 一种乐观锁的实现,监控key,如果key被修改了,后面的事务执行失败

multi
set k1 v1
set balance 110
exec # 有人加塞,修改了balance,事务不会执行
1
2
# 上面 watch 和 exec之间,有人加塞
set balance 300
1
2
# 放弃监控
unwatch

Redis管道

  • Redis是一种基于==客户端-服务器模型==的TCP服务

    • 客户端向服务器发送命令(发送命令,命令排队,命令执行,返回结果),并监听Socket返回,通常阻塞模式等待服务器响应
    • 服务器处理命令,并将结果返回客户端
    • 如果执行大量命令,就有大量RTT,性能不太好
  • pipeline可以一次发送多条命令给服务器,服务器依次处理完,一次性响应,减少了通信次数

    • 批处理命令变种优化措施,类似Redis的原生批命令(mget,mset)
1
2
3
4
5
6
7
8
9
cat cmd.txt
set k1 v1
set k2 v2
hset k3 name xdy
hset k3 age 12
hset k3 gender male
lpush list 1 2 3 4 5

cat cmd.txt | redis-cli -a 111111 --pipe

管道与原生批量命令对比

  • 原生批量命令有原子性,管道是非原子性
  • 原生批量命令一次只能执行一种命令,管道支持批量执行不同命令
  • 原生批量命令是服务端实现,管道需要服务端与客户端共同完成

管道与事务对比

  • 事务具有原子性,管道不具有原子性
  • 管道一次性将多条命令发送到服务器,事务一条一条发,接收到exec命令才执行
  • 执行事务会阻塞其他命令执行,执行管道则不会

使用管道注意事项

  • 管道中的指令只是会依次执行,不保证原子性;如果发生异常,会继续执行后续指令

  • 管道组装的命令个数不能太多,不然客户端阻塞的时间可能过久;服务端也被迫回复一个队列,占用内存

Redis发布订阅

了解即可

Redis复制

  • 是什么

    • 主从复制,master以写为主,slave以读为主

    • master数据变化时,自动将新的数据异步同步到其他slave数据库

  • 能干什么

    • 读写分离
    • 容灾恢复
    • 数据备份
    • redis7 主从 配置ubuntu水平扩容支撑高并发(一主多从)
  • 如何使用

    • 配从库,不配主库

    • master配置了requirepass参数,需要密码登录

      • slave配置masterauth设置校验密码

配置方式

  • 一个master,两个slave
    • (三台虚拟机,每台都安装redis)
  • 拷贝多个redis.conf
    • (redis6379.conf,redis6380.conf,redis6381.conf)
  • 配置文件需要修改的部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
port 6380						# 修改端口,6379,6380,6381...
daemonize yes # redis以守护线程的方式启动,会在后台运行
requirepass 123456 # 设置密码

# 其他机器访问
# bind 127.0.0.1 -::1 # 注释掉,bind:将指定的ip设置为redis的对外开放地址。如果设为127.0.0.1的话,只有本机能访问了
protected-mode no # 关闭保护模式,保护模式下外部网络无法访问

# 持久化
dir /myredis # 指定保存位置
dbfilename dump6380.rdb # RDB文件名
appendonly yes # 开启AOF(非必选)
appenddirname "appendonlydir" # 设置AOF文件夹名(非必须)

pidfile /var/run/redis_6380.pid # 该文件中记录了当前redis进程id

# 日志
loglevel notice # 日志级别
logfile "/myredis/6380.log" # 日志位置

# 从机设置
replicaof 172.26.238.112 6379 # 从机设置主机ip,port(从机必须,主机不需要)
masterauth 123456 # 从机访问主机的密码(从机必须,主机不需要)

一主二仆

配置文件固定写死

  • replicaof 主库IP 主库端口
  • 配从库不配主库
  • 先master,后两台slave依次启动
1
2
3
# 主库
redis-server ./redis6379.conf
redis-cli -a 123456
1
2
3
# 从库1,记得指定端口
redis-server ./redis6380.conf
redis-cli -a 123456 -p 6380
1
2
3
# 从库2,记得指定端口
redis-server ./redis6381.conf
redis-cli -a 123456 -p 6381
  • 日志
1
2
3
4
5
# 主机日志
vim /myredis/6379.conf

Synchronization with replica 172.24.252.134:6380 succeeded
Synchronization with replica 172.24.252.150:6381 succeeded
1
2
3
4
5
6
7
# 从机日志
tail -f /myredis/6380.conf

Connecting to MASTER 172.24.250.28:6379
MASTER <-> REPLICA sync: receiving streamed RDB from master with EOF to disk
MASTER <-> REPLICA sync: Flushing old data
MASTER <-> REPLICA sync: Loading DB in memory
  • 命令
1
info replication

命令控制手动指定

  • 从机去掉配置项目,3台主机目前都是主机状态,各不从属
1
# replicaof 172.26.238.112 6379
  • 从机上执行
1
slaveof 主库IP 主库端口
  • 配置:持久稳定
  • 命令:仅当次生效

一些问题

  1. 从机(salve)只能读不能写
  2. 数据复制:从机第一次一锅端;后续跟随,master写slave跟
  3. 主机shutdown,从机原地待命,等待主机重启归来
  4. 主机重启后,主从关系继续
  5. 从机shutdown,重启后能跟上大部队

薪火相传

  • 上一个slave可以是下一个slave的master
  • 中途变更转向:会清除之前的数据,重新建立拷贝最新的
1
slaveof 新主库IP 新主库端口

反客为主

1
slaveof no one

工作流程总结

slave启动,同步初请

  • slave启动成功连接到master后,会发送一个sync命令
  • slave首次连接到master,将执行全量复制

首次连接,全量复制

  • master收到sync命令,在后台RDB持久化,同时收集修改命令缓存起来。RDB持久化执行完毕后,将RDB快照和所有缓存命令发送给slave,完成同步
  • slave接收到数据,存盘并加载到内存,完成复制

心跳持续,保持通讯

  • master发出ping包,默认周期10秒
1
repl-ping-replica-period 10

进入平稳,增量复制

  • master继续将新收到的修改命令依次传给slave,完成同步

从机下线,重连续传

  • master和slave都会保存:复制的offset,masterId。offset保存在backlog中。master只会把已复制的offset后面的数据复制给slave,类似断点续传

复制的缺点

  • 复制延时,信号衰减
    • 写操作在master,同步更新到slave,同步有延时。系统繁忙,延时会更加严重。增加slave会使问题更严重
  • master挂了
    • master挂了,从机待命,系统只能读取不能写入
    • 需要人工介入

Redis哨兵

理论简介

  • 是什么
    • 监控后台master主机是否故障,如果故障,根据投票数自动将一个从库转换为新主库
  • 作用
    • 主从监控:监控主从库运行是否正常
    • 消息通知:将故障转移的结果发送给客户端
    • 故障转移:如果master异常,进行主从切换,将一个slave作为master
    • 配置中心:客户端通过哨兵获取当前Redis服务主节点的地址

案例实操

  • 三个哨兵:用于监控和维护集群,不存放数据
  • 1主2从:用于数据读取和存放
graph TB;
    Client<-->Sentinel集群
    subgraph Sentinel集群;
        direction RL
        Sentinel1----Sentinel2----Sentinel3
    end
    Sentinel集群<-->Redis主从复制
    subgraph Redis主从复制;
        direction TB
        Master<-->Slave1
        Master<-->Slave2
    end

配置哨兵

  • 这里使用了docker
1
2
3
4
# 哨兵
docker run -d -p 26379:26379 --name sentinel1 --privileged=true -v /dockerApp/sentinel1:/myredis redis redis-server /myredis/sentinel26379.conf --sentinel
docker run -d -p 26380:26380 --name sentinel2 --privileged=true -v /dockerApp/sentinel2:/myredis redis redis-server /myredis/sentinel26380.conf --sentinel
docker run -d -p 26381:26381 --name sentinel3 --privileged=true -v /dockerApp/sentinel3:/myredis redis redis-server /myredis/sentinel26381.conf --sentinel
  • 了解配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 重点参数
bind 服务监听地址,用于客户端连接,默认本机ip
daemonize 是否以后台daemon方式运行
protected-mode 安全保护模式
port 端口
logfile 日志文件
pidfile pid文件路径
dir 工作目录

# 前三个参数设计要监控的主机,quorum表示最少几个哨兵认为主机宕机,同意故障迁移的票数
sentinel monitor <master-name> <ip> <redis-port> <quorum>

# 连接master的密码
sentinel auth-pass <master-name> <password>
1
# 其他参数,不怎么需要管,自行差文档
  • /myredis目录下sentinel26379.conf文件
1
2
3
4
5
6
7
8
9
bind 0.0.0.0
daemonize yes
protected-mode no
port 26379
logfile "/myredis/sentinel26379.log"
pidfile "/myredis/sentinel26379.pid"
dir /myredis
sentinel monitor mymaster 192.168.114.40 6379 2
sentinel auth-pass mymaster 123456

配置主从机

  • 使用了docker
1
2
3
4
5
6
# 主机从机
docker run -d -p 6379:6379 --name redis1 --privileged=true -v /dockerApp/redis1:/myredis -v /dockerApp/redis1/data:/data redis redis-server /myredis/redis6379.conf

docker run -d -p 6380:6380 --name redis2 --privileged=true -v /dockerApp/redis2:/myredis -v /dockerApp/redis2/data:/data redis redis-server /myredis/redis6380.conf

docker run -d -p 6381:6381 --name redis3 --privileged=true -v /dockerApp/redis3:/myredis -v /dockerApp/redis2/data:/data redis redis-server /myredis/redis6381.conf
  • 主机从机配置redsi6379.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
port 6379
daemonize no # 为让redis不退出,必须有个前台程序
requirepass 123456

# bind 127.0.0.1 -::1 # 注释掉
protected-mode no

dir /myredis
dbfilename dump6379.rdb
appendonly yes
appenddirname "appendonlydir"

pidfile /myredis/redis6379.pid
loglevel notice
logfile "/myredis/6379.log"

# 主从机都设置,因为主机可能改变
masterauth 123456

# 从机设置
replicaof 192.168.114.40 6379

启动所有机器

  • 先启动主从,接着启动哨兵
1
2
3
4
5
6
7
8
9
# 哨兵日志
Sentinel ID is 24f3285df6c3b1019b8bcfc9bcd7132a5aa1363b

+monitor master mymaster 192.168.114.40 6379 quorum 2
+slave slave 172.17.0.1:6380 172.17.0.1 6380 @ mymaster 192.168.114.40 6379
+slave slave 172.17.0.1:6381 172.17.0.1 6381 @ mymaster 192.168.114.40 6379

+sentinel sentinel 15a1dd9afaacb46b89486a9e8417354d66b14563 172.17.0.6 26380 @ mymaster 192.168.114.40 6379
+sentinel sentinel 3f3c725be2765afb4a1afe807652c8b57056d0aa 172.17.0.7 26381 @ mymaster 192.168.114.40 6379

主机挂掉

  • 手动关掉6379,模拟挂掉了
  • 剩下两台机器自动选出了新的master
    • 这里info replication发现,6380成为了master
  • 查看哨兵的日志
1
2
3
4
5
6
7
8
+sdown master mymaster 192.168.114.40 6379
+odown master mymaster 192.168.114.40 6379 #quorum 2/2
+new-epoch 1
+vote-for-leader 24f3285df6c3b1019b8bcfc9bcd7132a5aa1363b 1

+switch-master mymaster 192.168.114.40 6379 172.17.0.1 6380
+slave slave 172.17.0.1:6381 172.17.0.1 6381 @ mymaster 172.17.0.1 6380
+slave slave 192.168.114.40:6379 192.168.114.40 6379 @ mymaster 172.17.0.1 6380 # 6379变为slave,即使回来了依然是slave
  • 6379恢复,会变成slave,不会冲突

一些问题

  • 主机刚挂掉,从机get时,会报
1
Error: Broken pipe

意思是:对端的管道断开了,远端把这个读/写管道关闭了,你无法继续对这个管道进行读写操作

  • 配置自动修改
    • 观察redis6379.conf, redis6380.conf可以发现,文件的内容被动态修改了
    • 配置文件会被sentinel动态更改
  • 哨兵可以监控多个主机
    • 自行去官网看文档

哨兵运行流程和选举原理

  • 三个哨兵,一主二从,正常运行中
  • SDown主观下线
    • 单个哨兵自己主观上。发送PING心跳一段时间没有收到合法回复
    • 配置文件:down-after-milliseconds(默认30s)
  • ODown客观下线
    • 多个哨兵达成一致意见
    • 配置文件:sentinel monitor <master-name> <ip> <redis-port> <quorum>
  • 选出领导者哨兵
    • 主节点被判断客观下线,各个哨兵协商选出领导者。
    • 由领导者进行failover(故障转移)
  • 领导者如何选择出来的?
    • Raft算法
    • 先到先得,哨兵A向哨兵B发送成为领导者的申请。
    • 如果B没同意过别人,就同意A成为领导者。
  • 领导者进行故障转移,并选出新的master
    • 新主登基:某一个slave被选为master
      • replica priority(权限)>replication offset(复制偏移量)>Run ID
    • 群臣俯首
      • 哨兵领导者对新master发送命令:slaveof no one,提升为master节点
      • 哨兵领导者对其他slave发送命令:slaveof,成为新的slave节点
    • 旧主拜服
      • 老master上线。哨兵领导者对其降级为slave,恢复工作

哨兵使用建议

  • 哨兵数量应为多个
  • 哨兵数量最好为奇数,好投票
  • 哨兵节点配置最好一致
  • 哨兵节点性能最好一致
  • 哨兵节点如果部署在Docker,要注意端口映射
  • 哨兵+主从复制,并不能保证数据不丢失

Redis集群

基本概念

  • 什么是集群
    • 数据量过大,单Master的复制集难以承担
    • 需要水平扩展,每个复制集只负责存储整个数据集的一部分
graph LR;
    Clinets-write-->M1
    Clinets-write-->M2
    Clinets-write-->M3
    subgraph Master
        M1
        M2
        M3
    end
    subgraph Slave
        S1
        S2
        S3
    end
    Master --> Slave
    S1-->Clinets-read
    S2-->Clinets-read
    S3-->Clinets-read
  • 能干什么
    • Redis集群支持多个Master,每个Master又可以挂载多个Slave
      • 读写分离
      • 支持数据高可用
      • 支持海量数据读写存储
    • 集群自带sentinel的故障转移机制,无需配置哨兵
    • 客户端与Redis节点连接时,只需任意连接集群中一个可用节点
    • 槽位slot负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系

集群算法-分片-槽位slot

槽位slot

  • Redis集群没有使用一致性hash,而是引入了哈希槽的概念
  • Redis集群有16384个哈希槽,每个key通过CRC16(key) mod 16384来决定放哪个槽

分片

  • 我们会将存储的数据分散到多台redis机器上,称为分片
  • 每个Redis实例都被认为时整个数据的一个分片
  • 比如有三个节点,0-16383,则分片:
    • 0-5460
    • 5461-10922
    • 10923-16383

分片和槽位的优势

  • 方便扩容和缩容
    • 假如有三台A,B,C
    • 比如移除A,则需要将A中的槽位匀一部分到B和C
  • 一个节点将哈希槽移动到另一个节点不会停止服务
    • 不会造成集群不可用

槽位映射的三种解决方案

哈希取余分区(小厂)
  • hash(key)/N
  • 优缺点
    • 优点:简单粗暴,让固定一部分请求落在同一台服务器上
    • 缺点:扩容和缩容后,映射需要重新计算,发生很大变化
一致性哈希分区
  • 目的:服务器个数发生变化,尽量少影响映射关系

  • 三大步骤

    1. 一致性哈希环
    • 一致性哈希对2^32^取模,形成一个虚拟圆环
    • [0,2^32^-1],0=2^32^-1,构成hash环
    1. 服务器IP节点映射
    • 将各IP映射到环上某位置,hash(IP)
    1. key落到各服务器规则
    • hash(key)计算数据在环上的位置
    • 顺时针走遇到的第一个服务器,就是应该存储的服务器
  • 优缺点

    • 优点:有一定容错性,有节点宕机,依然可以继续存储。扩展性好。
    • 缺点:节点太少分布不均匀,有数据倾斜问题
哈希槽分区
  • CRC16(key) % 16384

  • [0,2^14^-1]形成hash slot空间

  • 在数据和节点之间加了一层,称为哈希槽

  • 槽会分配给集群中所有主节点,分配策略没有要求

一些面试问题

为什么最大槽数是16384?

  • https://github.com/redis/redis/issues/2576

  • 如果槽位是65536,发送心跳的消息头达8k,过于庞大

    • 消息头中最占空间的是myslots[CLUSTER_SLOTS/8]
      • 槽位65536,大小为65536/8/1024=8kb
      • 槽位16384,大小为16384/8/1024=2kb
    • 每秒钟,redis节点需要发送一定数量的PING包,如果槽位65536,太大了浪费带宽
  • redis集群主节点数量基本不会超过1000个(网络杜塞),对1000以内的redis集群,16384个槽位够了

  • redis主节点负责的哈希槽是通过bitmap保存的。如果节点数少,哈希槽多,压缩率会很低

集群可能会写丢失

  • Redis集群不保证数据强一致性,意味着特定条件下,可能会丢掉一些写入请求命令

集群案例

  • 三主三从
  • 具体的分配是自动来的,未必真的长这样
graph LR;
    subgraph master
        6381
        6383
        6385
    end
    subgraph slave
        6382
        6384
        6386
    end
    6381-->6382
    6383-->6384
    6385-->6386

配置文件

  • /myreids/cluster6381.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/myredis/cluster6381.log"
pidfile "/myredis/cluster6381.pid"
dir /myredis
dbfilename dump6381.rdb
appendonly yes
appendfilename "appendonly6381.aof"
requirepass 123456
masterauth 123456

cluster-enabled yes # 集群是否开启
cluster-config-file nodes-6381.conf # 集群配置文件
cluster-node-timeout 5000 # 集群超时时间

启动6台redis主机

1
2
3
redis-server /myreids/cluster6381.conf
...
redis-server /myreids/cluster6386.conf

通过redis-cli为6台机器构建集群关系

1
redis-cli -a 123456 --cluster create --cluster-replicas 1 <地址1:端口1> <地址2:端口2> <地址3:端口3> <地址4:端口4> <地址5:端口5> <地址6:端口6>
  • –cluster create:表示集群形式创建

  • –cluster-replicas 1:每个master一个slave

  • 集群联通成功后,产生nodes-xxxx.conf配置文件

检验集群关系

  • info replication
  • cluster nodes:查看集群节点信息

读写数据

  • 一定注意槽位的范围,需要路由到位(加参数-c)
1
redis-cli -a 111111 -p 6381 -c
  • 查看某个key属于的槽位
1
cluster keyslot <key>

主从容错切换迁移案例

  • 6381宕机,cluster nodes查看

    • 直接从机6382上位成为master
  • 6381恢复,cluster nodes查看

    • 江山易主,变成slave,master是6382
  • 6381,6382主从关系颠倒,如果想恢复原有关系(故障转移)

1
cluster failover

集群扩容

graph LR;
    subgraph master
        6381
        6383
        6385
        6387-新
    end
    subgraph slave
        6382
        6384
        6386
        6388-新
    end
    6381-->6382
    6383-->6384
    6385-->6386
    6387-新-->6388-新
  • 新启动redis实例,此时还不在集群中
1
2
redis-server /myreids/cluster6387.conf
redis-server /myreids/cluster6388.conf
  • 将新增的6387作为master节点加入原有集群
    • 6381相当于原来集群里的引路人
1
redis-cli -a 123456 --cluster add-node <地址7:6387> <地址1:6381>
  • 检查集群情况
    • 发现还未为6387分配槽位
1
redis-cli -a 123456 --cluster check <地址1:6381>
  • 重新分配槽位
    • How many slots do you want to move? 4096
    • What is the receving node ID: <6387节点的id>
    • Source node #1: all
    • 分完再查看集群情况,可以看到6387被分配了许多段的槽位
1
redis-cli -a 123456 --cluster reshard <地址1:6381>
  • 将新增的6388作为slave节点加入原有集群
1
redis-cli -a 123456 --cluster add-node <地址8:6388> <地址7:6387> --cluster-slave --cluster-master-id <6387主机节点id>

集群缩容

  • 清除master节点6388
1
2
3
4
5
# 获得6388,6387,6381的id
redis-cli -a 123456 --cluster check <地址1:6381>

# 删除6388节点
redis-cli -a 123456 --cluster del-node <地址8:6388> <6388节点的id>
  • 清除6387槽号,本例中,重新分配给6381
    • How many slots do you want to move? 4096
    • What is the receving node ID: <6381节点的id>
    • Source node #1: <6387节点的id>
    • Source node #2: done
1
redis-cli -a 123456 --cluster reshard <地址1:6381>
  • 此时6387由6381变成slave,挂在6381下面

  • 删除slave节点6387

1
redis-cli -a 123456 --cluster del-node <地址7:6387> <6387节点的id>
  • 恢复完毕,三主三从

集群常用操作和CRC16算法分析

通识占位符

  • 不在同一个slot槽位下的键值无法使用mset,mget等多键操作
  • 通识占位符登场
1
2
3
4
# 会分配到同一个槽位上,具体用什么占位符自定义
mset k1{z} v1 k2{z} v2 k3{z} v3

mget k1{z} k2{z} k3{z}

常用配置/命令

  • 集群是否完整才能对外提供服务
    • 原本三主三从。二主三从(可以服务);二主二从(集群不完整了,不服务)
1
2
# 配置文件中
cluster-require-full-coverage yes
  • 查看槽位是否被占用
    • 1:被占用
    • 0:没被占用
1
cluster countkeysinslot 12706
  • 查看键应该存在哪个槽位上
1
cluster keyslot <key>

SpringBoot整合Redis

总体概述

  • jedis
    • 相当于jdbc
  • lettuce
    • 对jedis进一步优化
  • RedisTemplate(推荐使用)
    • 进一步对lettuce做了封装

集成jedis

  • jedis client是Redis官网推荐的,面向java的客户端,实现了对各类API的封装
  • 引入依赖
1
2
3
4
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获得连接
Jedis jedis = new Jedis("192.168.114.40", 6379);

// 指定密码
jedis.auth("123456");

// 访问redis
System.out.println(jedis.ping());

// keys
Set<String> keys = jedis.keys("*");
long ttl = jedis.ttl("k1");
long r1 = jedis.expire("k1", 20);

// string
String k1 = jedis.get("k1");

// list
long r2 = jedis.lpush("l1", "11", "12", "13");
List<String> l1 = jedis.lrange("l1", 0, -1);

// 其他api自行探索

集成lettuce

  • 使用jedis每次都需要new一个
    • 需要”池化技术“
  • SpringBoot2.0之后,都默认使用Lettuce这个客户端连接Redis服务器
    • Lettuce底层使用的是Netty
  • 引入依赖
1
2
3
4
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用构建器链式编程来build我们的RedisURI
RedisURI uri = RedisURI.builder()
.withHost("192.168.114.40")
.withPort(6379)
.withPassword("123456")
.build();
// 创建连接客户端
RedisClient redisClient = RedisClient.create(uri);
// 获得连接
StatefulRedisConnection<String, String> connect = redisClient.connect();
// 创建操作command
RedisCommands<String, String> commands = connect.sync();

// 业务逻辑
List<String> l1 = commands.lrange("l1", 0, -1);
System.out.println(l1);

// 各种关闭,释放资源
connect.close();
redisClient.shutdown();

集成RedisTemplate集成

  • 前两种了解即可,这个必须熟悉
  • 导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--SpringBoot与Redis整合-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
  • spring-boot-starter-data-redis已帮我们引入了lettuce

连接单机

  • 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server.port=7777

spring.application.name=redis7_study

# ========================logging=====================
logging.level.root=info
logging.level.com.example.myredis=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

logging.file.name=C:/myredis/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

# ========================swagger=====================
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# ========================redis单机=====================
spring.redis.database=0
spring.redis.host=192.168.114.40
spring.redis.port=6379
spring.redis.password=123456
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
  • 配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();

redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

redisTemplate.afterPropertiesSet();

return redisTemplate;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
@EnableSwagger2
public class SwaggerConfig
{
@Value("${spring.swagger2.enabled}")
private Boolean enabled;

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(enabled)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.myredis")) //你自己的package
.paths(PathSelectors.any())
.build();
}
public ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
.description("springboot+redis整合,有问题给管理员发邮件:1625378509@qq.com")
.version("1.0")
.termsOfServiceUrl("https://www.atguigu.com/")
.build();
}
}
  • 使用
1
2
3
4
5
6
public static final String ORDER_KEY = "ord:";

String key = ORDER_KEY + keyId;
redisTemplate.opsForValue().set(key, value);

redisTemplate.opsForValue().get(ORDER_KEY + keyId);
  • 问题

key和value都是通过Spring提供的Serialize序列化到数据库的

  • RedisTemplate默认使用JdkSerializationRedisSerializer

  • StringRedisTemplate默认使用StringRedisSerializer

redis-cli中使用中文

1
redis-cli -a 123456 -p 6379 --raw

连接集群

  • 标题: Redis基础
  • 作者: 布鸽不鸽
  • 创建于 : 2023-08-28 18:44:58
  • 更新于 : 2023-08-28 18:46:34
  • 链接: https://xuedongyun.cn//post/26216/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论