前言 本文是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
类型的无序集合,成员是唯一的集合对象的编码可以是intset
或hashtable
(后面会讲) 底层是通过Hash
实现的 最多包含2^32-1个元素(约40亿) Sorted set
(zset
)不同的是,每个元素都会关联一个double
类型的分数,从小到大排序 Geospatial
存储地理位置 可以操作:添加地理位置的坐标,获取地理位置的坐标,计算位置间的距离,根据给定经纬度坐标获取指定范围内地理位置集合 Hyperlog
基数统计 输入元素非常大时,计算基数所需的空间总是固定且很小的 每个HyperLogLog
键只需要12KB,就可以计算2^64
个不同元素的基数 只会根据输入元素计算基数,不会存储元素本身 Bitmap
Bitfield
可以一次性操作多个比特位域(连续多个比特位),会执行一系列操作并返回一个响应数组 Stream
命令查询 常用key操作命令 1 2 exists k1 exists k1 k2 k3
非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除将在后续异步中进行 查看还有多少秒过期,-1表示永不过期,-2表示已过期 将当前数据库key移动到给定index(0-15)的数据库中 数据类型命令及落地应用 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 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 list列表 底层是一个双端链表
1 2 3 lpush list1 1 2 3 4 // 返回修改数 rpush list2 1 2 3 4 lrange list start end // 一定范围内遍历,没有rrange
1 lindex list1 0 // 没有rindex
1 rpoplpush source destination // 返回修改的那个值
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代替
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 hgetall shopcar:uuid1024
set集合 1 sadd key member1 member2
1 srem key member1 member2
将一个元素从一个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个
1 zscore key member // 返回分值
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
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
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
1 pfadd key element1 element2
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
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)+阻塞队列
1 2 3 lpush mylist a b c d e rpop mylist rpop mylist
pub/sub发布和订阅机制实现消息队列:一对多模式 缺点:消息无法持久化,如果网络断开,宕机等,消息就会丢失;而且也没有ACK机制保证数据可靠性 能干什么
实现消息队列 支持消息持久化 支持自动生成全局唯一id 支持ack确定消息的模式 支持消费组模式 …让消息队列更加稳定可靠 底层结构
名称 内涵 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 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的详细信息 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方式 以指定的时间间隔,执行数据集的时间点快照
保存时间间隔,配置文件(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 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
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
1 2 3 4 5 6 7 redis-server ./redis-my.config redis-cli // 获取配置,查看修改成功与否 config get save config get dir config get dbfilename
自动触发 手动触发 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
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
手动触发
AOF文件重写并不是对原文件重新整理,而是直接读取服务器现有键值对,生成新的AOF文件代替原来AOF文件
AOF重写原理 redis fork一个子进程。子进程基于当前内存中的数据,构建日志,往新的临时AOF文件中写入日志 开启AOF重写缓冲区。保存这段时间内新增的写指令 主进程继续写旧文件。写入AOF重写缓冲区,进而继续写入旧AOF文件 AOF重写缓冲区的指令追加到新文件 新AOF文件替代旧AOF文件 RDB-AOF混合持久化 1 2 # 混合模式默认开启 aof-use-rdb-preamble yes
数据恢复流程和加载顺序 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混合使用 纯缓存模式 1 2 3 4 5 # 禁用rdb持久化模式下,仍可以使用save,bgsave命令生成rdb文件 save "" # 禁用AOF功能,仍可以使用bgrewriteaof生成aof文件 appendonly no
Redis事务 依次执行多个命令,本质就是一组命令的集合 一个事务中所有命令都会序列化,按顺序串行执行,不会被其他命令加塞 Redis事务vs数据库事务 单独的隔离操作redis事务仅仅保证事务中操作会被连续独占的执行。(redis命令执行是单线程架构) 没有隔离级别的概念 不保证原子性redis不保证所有指令同时成功或失败,只有决定是否开始执行全部指令的能力,没有回滚的能力 排他性redis会保证一个事务内的命令依次执行,不被其他命令插入 使用案例 1 2 3 4 multi set k1 v1 expire k1 30 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
Redis管道 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复制 是什么
能干什么
读写分离 容灾恢复 数据备份 redis7 主从 配置ubuntu水平扩容支撑高并发(一主多从) 如何使用
配置方式 一个master,两个slave 拷贝多个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
命令控制手动指定 从机去掉配置项目,3台主机目前都是主机状态,各不从属 1 # replicaof 172.26.238.112 6379
一些问题 从机(salve)只能读不能写 数据复制:从机第一次一锅端;后续跟随,master写slave跟 主机shutdown,从机原地待命,等待主机重启归来 主机重启后,主从关系继续 从机shutdown,重启后能跟上大部队 薪火相传 上一个slave可以是下一个slave的master 中途变更转向:会清除之前的数据,重新建立拷贝最新的 反客为主 工作流程总结 slave启动,同步初请 slave启动成功连接到master后,会发送一个sync命令 slave首次连接到master,将执行全量复制 首次连接,全量复制 master收到sync命令,在后台RDB持久化,同时收集修改命令缓存起来。RDB持久化执行完毕后,将RDB快照和所有缓存命令发送给slave,完成同步 slave接收到数据,存盘并加载到内存,完成复制 心跳持续,保持通讯 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 配置哨兵 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>
/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
配置主从机 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
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
一些问题 意思是:对端的管道断开了,远端把这个读/写管道关闭了,你无法继续对这个管道进行读写操作
配置自动修改观察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被选为masterreplica 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
优缺点优点:简单粗暴,让固定一部分请求落在同一台服务器上 缺点:扩容和缩容后,映射需要重新计算,发生很大变化 一致性哈希分区 目的:服务器个数发生变化,尽量少影响映射关系
三大步骤
一致性哈希环 一致性哈希对2^32^取模,形成一个虚拟圆环 [0,2^32^-1],0=2^32^-1,构成hash环 服务器IP节点映射 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>
检验集群关系 info replication cluster nodes:查看集群节点信息 读写数据 1 redis-cli -a 111111 -p 6381 -c
主从容错切换迁移案例 集群扩容 graph LR;
subgraph master
6381
6383
6385
6387-新
end
subgraph slave
6382
6384
6386
6388-新
end
6381-->6382
6383-->6384
6385-->6386
6387-新-->6388-新 1 2 redis-server /myreids/cluster6387.conf redis-server /myreids/cluster6388.conf
1 redis-cli -a 123456 --cluster add-node <地址7:6387> <地址1:6381>
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>
1 redis-cli -a 123456 --cluster add-node <地址8:6388> <地址7:6387> --cluster-slave --cluster-master-id <6387主机节点id>
集群缩容 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槽号,本例中,重新分配给6381How 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>
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 cluster countkeysinslot 12706
SpringBoot整合Redis 总体概述 jedis lettuce RedisTemplate(推荐使用) 集成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" ); System.out.println(jedis.ping()); Set<String> keys = jedis.keys("*" ); long ttl = jedis.ttl("k1" );long r1 = jedis.expire("k1" , 20 );String k1 = jedis.get("k1" );long r2 = jedis.lpush("l1" , "11" , "12" , "13" );List<String> l1 = jedis.lrange("l1" , 0 , -1 );
集成lettuce 使用jedis每次都需要new一个 SpringBoot2.0之后,都默认使用Lettuce这个客户端连接Redis服务器 引入依赖 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 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(); 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 <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 > <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.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 spring.swagger2.enabled =true spring.mvc.pathmatch.matching-strategy =ant_path_matcher 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); redisTemplate.setKeySerializer(new StringRedisSerializer ()); 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" )) .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序列化到数据库的
redis-cli中使用中文
1 redis-cli -a 123456 -p 6379 --raw
连接集群