# Redis学习笔记
**Repository Path**: HaiXiuDeDXianSheng/redis-learning-notes
## Basic Information
- **Project Name**: Redis学习笔记
- **Description**: 用于记录学习Redis过程中做的笔记
- **Primary Language**: Java
- **License**: GPL-3.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 2
- **Created**: 2022-10-08
- **Last Updated**: 2025-02-12
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
- [1. Redis 基础](#1-redis-基础)
- [1.1. NoSQL 简介](#11-nosql-简介)
- [1.1.1. 常见 NoSQL 数据库](#111-常见-nosql-数据库)
- [1.2. Redis 应用场景](#12-redis-应用场景)
- [1.3. Redis 安装](#13-redis-安装)
- [2. Redis数据类型](#2-redis数据类型)
- [2.1. 对 key 的操作](#21-对-key-的操作)
- [2.2. 对字符串 string 的操作](#22-对字符串-string-的操作)
- [2.3. 对 hash 表的操作](#23-对-hash-表的操作)
- [2.4. 对 list 的操作](#24-对-list-的操作)
- [2.5. 对 set 的操作](#25-对-set-的操作)
- [2.6. 对 ZSet 的操作(重要)](#26-对-zset-的操作重要)
- [2.7. 对位图 BitMap 的操作](#27-对位图-bitmap-的操作)
- [2.7.1. 设置值](#271-设置值)
- [2.7.2. 获取值](#272-获取值)
- [2.7.3. 获取Bitmaps指定范围值为1的个数](#273-获取bitmaps指定范围值为1的个数)
- [2.7.4. Bitmap 间的运算](#274-bitmap-间的运算)
- [2.8. 对 HyperLogLog 结构的操作](#28-对-hyperloglog-结构的操作)
- [2.8.1. HyperLogLog 应用场景](#281-hyperloglog-应用场景)
- [2.8.2. HyperLogLog为什么适合做大量数据的统计](#282-hyperloglog为什么适合做大量数据的统计)
- [2.9. Redis Java API](#29-redis-java-api)
- [2.9.1. 创建 Maven 工程](#291-创建-maven-工程)
- [2.9.2. 操作 string 类型数据](#292-操作-string-类型数据)
- [2.9.3. 操作 hash 类型数据](#293-操作-hash-类型数据)
- [2.9.4. 操作 list 类型数据](#294-操作-list-类型数据)
- [2.9.5. 操作set类型的数据](#295-操作set类型的数据)
- [3. Redis 持久化](#3-redis-持久化)
- [3.1. RDB 持久化方案](#31-rdb-持久化方案)
- [3.1.1. 优缺点](#311-优缺点)
- [3.1.2. RDB 配置](#312-rdb-配置)
- [3.2. AOF 持久化方案](#32-aof-持久化方案)
- [3.2.1. 配置 AOF](#321-配置-aof)
- [3.2.2. AOF rewrite](#322-aof-rewrite)
- [3.2.3. AOF 优缺点](#323-aof-优缺点)
- [3.3. RDB VS AOF](#33-rdb-vs-aof)
- [4. Redis 事务](#4-redis-事务)
- [4.1. 事务相关命令](#41-事务相关命令)
- [4.2. Redis 事务演示](#42-redis-事务演示)
- [4.2.1. 事务成功演示](#421-事务成功演示)
- [4.2.2. 事务失败演示](#422-事务失败演示)
- [4.3. 为什么 Redis 不像 Mysql 一样支持事务回滚?](#43-为什么-redis-不像-mysql-一样支持事务回滚)
- [5. 数据删除与淘汰策略](#5-数据删除与淘汰策略)
- [5.1. 过期数据](#51-过期数据)
- [5.1.1. Redis中的数据特征](#511-redis中的数据特征)
- [5.1.2. 时效性数据的存储结构](#512-时效性数据的存储结构)
- [5.2. 数据删除策略](#52-数据删除策略)
- [5.2.1. 数据删除策略的目标](#521-数据删除策略的目标)
- [5.2.2. 定时删除](#522-定时删除)
- [5.2.3. 惰性删除](#523-惰性删除)
- [5.2.4. 定期删除](#524-定期删除)
- [5.3. 数据淘汰策略-逐出算法 (面试重点)](#53-数据淘汰策略-逐出算法-面试重点)
- [5.3.1. 淘汰策略概述](#531-淘汰策略概述)
- [5.3.2. 策略配置](#532-策略配置)
- [5.3.2.1. 最大可用内存](#5321-最大可用内存)
- [5.3.2.2. 每次选取待删除数据的个数](#5322-每次选取待删除数据的个数)
- [5.3.2.3. 对数据进行删除的选择策略](#5323-对数据进行删除的选择策略)
- [5.3.2.3.1. 第一类: 检测易失数据](#53231-第一类-检测易失数据)
- [5.3.2.3.2. 第二类: 检测全库数据](#53232-第二类-检测全库数据)
- [5.3.2.3.3. 放弃数据驱逐](#53233-放弃数据驱逐)
- [5.3.3. 淘汰策略配置依据](#533-淘汰策略配置依据)
- [6. Redis主从复制架构](#6-redis主从复制架构)
- [6.1. 主从复制简介](#61-主从复制简介)
- [6.1.1. 主从复制概念](#611-主从复制概念)
- [6.1.2. 主从复制的作用](#612-主从复制的作用)
- [6.2. 主从复制工作流程 (三个阶段)](#62-主从复制工作流程-三个阶段)
- [6.2.1. 阶段一: 建立连接](#621-阶段一-建立连接)
- [6.2.1.1. master 和 slave 互联](#6211-master-和-slave-互联)
- [6.2.1.2. 主从断开连接](#6212-主从断开连接)
- [6.2.1.3. 授权访问](#6213-授权访问)
- [6.2.2. 阶段二: 数据同步](#622-阶段二-数据同步)
- [6.2.2.1. 数据同步阶段 master 说明](#6221-数据同步阶段-master-说明)
- [6.2.2.2. 数据同步阶段 slave 说明](#6222-数据同步阶段-slave-说明)
- [6.2.3. 阶段三: 命令传播](#623-阶段三-命令传播)
- [6.2.3.1. 命令传播阶段的部分复制](#6231-命令传播阶段的部分复制)
- [6.2.3.1.1. 服务器运行ID(runid)](#62311-服务器运行idrunid)
- [6.2.3.1.2. 复制缓冲区](#62312-复制缓冲区)
- [6.2.4. 流程更新 (全量复制/部分复制)](#624-流程更新-全量复制部分复制)
- [6.2.5. 心跳机制](#625-心跳机制)
- [6.3. 搭建主从架构](#63-搭建主从架构)
- [6.4. 主从架构常见问题 (架构师须知)](#64-主从架构常见问题-架构师须知)
- [6.4.1. 频繁全量复制](#641-频繁全量复制)
- [6.4.2. 频繁的网络中断](#642-频繁的网络中断)
- [6.4.3. 数据不一致](#643-数据不一致)
- [7. Redis 的 Sentinel 架构](#7-redis-的-sentinel-架构)
- [7.1. 哨兵模式简介](#71-哨兵模式简介)
- [7.1.1. 哨兵概念](#711-哨兵概念)
- [7.2. 启动哨兵模式](#72-启动哨兵模式)
- [7.3. 搭建哨兵模式](#73-搭建哨兵模式)
- [7.4. java API 连接 Sentinel](#74-java-api-连接-sentinel)
- [7.4.1. 原生java代码](#741-原生java代码)
- [7.4.2. spring boot方式连接](#742-spring-boot方式连接)
- [7.5. 哨兵工作原理 (面试)](#75-哨兵工作原理-面试)
- [7.5.1. 监控](#751-监控)
- [7.5.2. 通知](#752-通知)
- [7.5.3. 故障转移](#753-故障转移)
- [8. Redis Cluster 集群](#8-redis-cluster-集群)
- [8.1. 集群简介](#81-集群简介)
- [8.2. Cluster 集群结构设计](#82-cluster-集群结构设计)
- [8.3. Cluster 集群结构搭建](#83-cluster-集群结构搭建)
- [8.4. java API 操作 Redis Cluster](#84-java-api-操作-redis-cluster)
- [8.4.1. 原生java代码操作](#841-原生java代码操作)
- [9. Redis 高频面试题](#9-redis-高频面试题)
- [9.1. 缓存预热 (提前加载缓存数据)](#91-缓存预热-提前加载缓存数据)
- [9.1.1. 场景](#911-场景)
- [9.1.2. 问题排查](#912-问题排查)
- [9.1.3. 解决方案](#913-解决方案)
- [9.2. 缓存雪崩 (大量 key 集中过期)](#92-缓存雪崩-大量-key-集中过期)
- [9.2.1. 场景](#921-场景)
- [9.2.2. 问题排查](#922-问题排查)
- [9.2.3. 解决方案](#923-解决方案)
- [9.3. 缓存击穿 (热点key过期,访问量巨大)](#93-缓存击穿-热点key过期访问量巨大)
- [9.3.1. 场景](#931-场景)
- [9.3.2. 问题排查](#932-问题排查)
- [9.3.3. 解决方案](#933-解决方案)
- [9.4. 缓存穿透 (查了不存在的数据,key大面积未命中)](#94-缓存穿透-查了不存在的数据key大面积未命中)
- [9.4.1. 场景](#941-场景)
- [9.4.2. 问题排查](#942-问题排查)
- [9.4.3. 问题分析](#943-问题分析)
- [9.4.4. 解决方案](#944-解决方案)
- [9.5. Redis 命名规范](#95-redis-命名规范)
- [9.6. 性能指标监控](#96-性能指标监控)
# 1. Redis 基础
## 1.1. NoSQL 简介
- 区别于关系数据库,NoSql 不保证关系数据的 ACID 特性
- 应用场景
- 高并发读写:10W/s
- 海量数据读写
- 高扩展性,不限制语言
- 可以用 lua 脚本进行增强
- 速度快
- 不适合场景
- 需要事务支持
- 基于sql的结构化查询存储,处理复杂的关系,需要即席查询(用户自定义查询条件的查询)
- 政府银行金融项目,还是使用关系型数据库。
### 1.1.1. 常见 NoSQL 数据库
- Redis
- 数据都在内存中,支持持久化,主要用作备份恢复
- 除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
- 一般是作为缓存数据库辅助持久化的数据库
- mongoDB
- 高性能、开源、模式自由(schema free)的文档型数据库
- 数据都在内存中, 如果内存不足,把不常用的数据保存到硬盘
- 虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
- 支持二进制数据及大型对象
- 可以根据数据的特点替代RDBMS,成为独立的数据库。或者配合RDBMS,存储特定的数据。
- HBase
- 它用于需要对大量的数据进行随机、实时读写操作的场景中
- 可以用普通的计算机处理超过10亿行数据,还可处理有数百万列元素的数据表
## 1.2. Redis 应用场景
- **取最新N个数据的操作**
- 比如典型的取网站最新文章,可以将最新的5000条评论ID放在Redis的List集合中,并将超出集合部分从数据库获取
- **排行榜应用,取TOP N操作**
- 这个需求与上面需求的不同之处在于,前面操作以时间为权重,这个是以某个条件为权重,比如按顶的次数排序,可以使用Redis 的 sorted set,将要排序的值设置成 sorted set 的 score,将具体的数据设置成相应的 value,每次只需要执行一条 ZADD 命令即可
- **需要精准设定过期时间的应用**
- 比如可以把上面说到的 sorted set 的 score 值设置成过期时间的时间戳,那么就可以简单地通过过期时间排序,定时清除过期数据了,不仅是清除Redis中的过期数据,你完全可以把 Redis 里这个过期时间当成是对数据库中数据的索引,用Redis来找出哪些数据需要过期删除,然后再精准地从数据库中删除相应的记录
- **计数器应用**
- Redis 的命令都是原子性的,你可以轻松地利用 INCR,DECR 命令来构建计数器系统
- **Uniq操作,获取某段时间所有数据排重值**
- 这个使用 Redi s的 set 数据结构最合适了,只需要不断地将数据往 set 中扔就行了,set 意为集合,所以会自动排重
- **实时系统,反垃圾系统**
- 通过上面说到的 set 功能,你可以知道一个终端用户是否进行了某个操作,可以找到其操作的集合并进行分析统计对比等。没有做不到,只有想不到
- **缓存**
- 将数据直接存放到内存中,性能优于Memcached,数据结构更多样化
## 1.3. Redis 安装
- Redis 安装
- 详见 [参考资料](https://gitee.com/HaiXiuDeDXianSheng/software-installation-notes/tree/master/redis%E5%88%86%E5%B8%83%E5%BC%8F%E5%AE%89%E8%A3%85#%E5%A4%9A%E9%9B%86%E7%BE%A4%E5%AE%89%E8%A3%85)
# 2. Redis数据类型
- redis 中一共支持 5 中数据类型
- string 字符串
- blob/bitmap 本质上存的也是 string 类型
- list 列表
- set 集合
- hash 表
- zset 有序集合
## 2.1. 对 key 的操作
|序号| 命令|描述 |示例|
|:-:|:-:|:-:|:-:|
|1 |keys pattern |查看指定模式的 key |示例:keys * 查看所有 key,生产环境中不要使用 keys *|
|2 |DUMP key |序列化给定 key ,并返回被序列化的值。 |示例:DUMP key1|
|3 |EXISTS key |检查给定 key 是否存在。 |示例:exists ydlclass|
|4 |EXPIRE key seconds |为给定 key 设置过期时间,以秒计。 |示例:expire ydlclass 5, 时间过了就过期了|
|5 |PEXPIRE key milliseconds |设置 key 的过期时间以毫秒计。|示例:PEXPIRE set3 3000|
|6 |PTTL key |以**毫秒**为单位返回 key 的剩余的过期时间,即还有多少时间过期。 |示例:pttl set2|
|7 |TTL key |以**秒**为单位,返回给定 key 的剩余生存时间(TTL, time to live)。| 示例:ttl set2|
|8 |RANDOMKEY |从当前数据库中随机返回一个 key。可用于抽样调查 |示例: randomkey|
|9 |TYPE key |返回 key 所储存的值的类型。|示例:type set10|
>```bash
>127.0.0.1:6379> del zhd
>(integer) 1
>127.0.0.1:6379> lrange zhd 0 -1
>(empty list or set)
>127.0.0.1:6379> keys *
>1) "szhd3"
>2) "szhd2"
>3) "szhd"
>127.0.0.1:6379> exists zhd
>(integer) 0
>```
- expire 案例
>```bash
>127.0.0.1:6379> set zhd 11111
>OK
>127.0.0.1:6379> expire zhd 5
>(integer) 1
>127.0.0.1:6379> pttl zhd # 查看剩余过期时间,可以用在登录的续期上面。
>(integer) 2606 # 返回 -1 表示永久存在
>127.0.0.1:6379> get zhd # 5 秒后查询
>(nil)
>```
## 2.2. 对字符串 string 的操作
|序号| 命令|描述 |示例|
|:-:|:-|:-:|:-|
|1 |SET key value |设置指定 key 的值 |SET hello world EX 3600,1h 后过期|
|2 |GET key |获取指定 key 的值。 |GET hello|
|3 |GETSET key value |将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 |GETSET hello world2|
|4 |MGET key1 [key2..] |获取所有(一个或多个)给定 key 的值。 |MGET hello world|
|5 |MSET key value [key value ] |同时设置一个或多个 key-value 对。|MSET ydlclass2 ydlclassvalue2 ydlclass3 ydlclassvalue3|
|6 |SETNX key value |只有在 key 不存在时设置 key 的值。 |SETNX ydlclass redisvalue|
|7 |MSETNX key1 value1 key2 value2 |同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 |MSETNX ydlclass4 ydlclassvalue4 ydlclass5 ydlclassvalue5|
|8 |STRLEN key |返回 key 所储存的字符串值的长度。 |STRLEN ydlclass|
|9 |PSETEX key milliseconds value|这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。 |PSETEX ydlclass6 6000 ydlclass6value|
|10 |INCR key |将 key 中储存的数字值增一。 | set ydlclass7 1 ;INCR ydlclass7 GET ydlclass7|
|11 |INCRBY key increment |将 key 所储存的值加上给定的增量值(increment) |INCRBY ydlclass7 2 ;get ydlclass7|
|12 |INCRBYFLOAT key increment |将 key 所储存的值加上给定的浮点增量值(increment) |INCRBYFLOAT ydlclass7 0.8|
|13 |DECR key |将 key 中储存的数字值减一。 | set ydlclass8 1 DECR ydlclass8 GET ydlclass8|
|14 |DECRBY key decrement |key 所储存的值减去给定的减量值(decrement) |DECRBY ydlclass8 3|
|15 |APPEND key value |如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。 |APPEND ydlclass8 hello|
|16 SETEX key seconds value |将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。 |SETEX hello 10 world3|
- 在使用 redis 进行累加或者累减操作时,**不要通过 get set 多步指令完成,而需要通过一条 inr 或者 decr 指令完成**
- 不然涉及多线程和事务时,会造成数据隔离的问题。因为线程 A、B 同时 get 了数据,A 对数据进行操作,重新 set 回去;理论上 B 应该在此基础上进行操作,可是 B 在 get 时数据还是之前的数据,再进行 set 就会有问题。
## 2.3. 对 hash 表的操作
- Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
- Redis 中每个 hash 可以存储 2的32 - 1 键值对(40多亿)
|序号 |命令|描述 |示例|
|:-:|:-:|:-:|:-:|
|1 |HSET key field value |将哈希表 key 中的字段 field 的值设为 value。(key就是上图的大 key,field 就是上图的小 key)|hset user username itlils, hset user age 18|
|2 |HGET key field |获取存储在哈希表中指定字段的值。|HGET key1 field4|
|3 |HSETNX key field value |只有在字段 field 不存在时,设置哈希表字段的值。|HSETNX key1 field2 value2|
|4 |HMSET key field1 value1 [field2 value2 ] |同时将多个 field-value (域-值)对设置到哈希表 key 中, 批量设置。|HMSET key1 field3 value3 field4 value4|
|5 |HGET key field |获取存储在哈希表中指定字段的值。 |HGET key1 field4|
|6 |HGETALL key |获取在哈希表中指定 key 的所有字段和值 |HGETALL key1|
|7 |HMGET key field1 [field2] |获取所有给定字段的值| HMGET key1 field3 field4|
|8 |HKEYS key |获取所有哈希表中的字段 |HKEYS key1|
|9 |HVALS key |获取哈希表中所有值 |HVALS key1|
|10 |HINCRBY key field increment |为哈希表 key 中的指定字段的整数值加上增量 increment 。|HSET key2 field1 1, HINCRBY key2 field1 1 ,HGET key2 field1|
|11 |HINCRBYFLOAT key field increment |为哈希表 key 中的指定字段的浮点数值加上增量 increment 。|HINCRBYFLOAT key2 field1 0.8|
|12 |HLEN key |获取哈希表中字段的数量 |HLEN key1|
|13 |HDEL key field1 [field2] |删除一个或多个哈希表字段| HDEL key1 field3, HVALS key1|
## 2.4. 对 list 的操作
- Redis列表是简单的字符串列表,按照插入 **顺序** 排序。(底层存的是 Linked List)
- 可以添加一个元素到列表的头部(左边)或者尾部(右边)
- 这顺序根据插入的顺序而定,**lpush 从左往右插入,则右边是头;rpush 从右往左插入, 则左边是头**
- 以栈顶为 index = 0 往栈底计数
- 一个列表最多可以包含 2 的 32 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
- 可以使用 list 结构实现队列和栈
- 一个程序一直 lpush 值,一个程序一直 lpop 值
|序号| 命令|描述 |示例|
|:-:|:-:|:-:|:-:|
|1 |LPUSH key value1 [value2] |将一个或多个值插入到列表头部 |LPUSH list1 value1 value2|
|2 |LRANGE key start stop |查看list当中所有的数据,start 和 stop 是从头部开始计数的 |LRANGE list1 0 -1, 看全部的数据|
|3 |RPUSH key value1 [value2] |在列表中添加一个或多个值到尾部 |RPUSH list1 value4 value5 LRANGE list1 0 -1|
|4 |RPUSHX key value |为已存在的列表添加单个值到尾部 |RPUSHX list1 value6|
|5 |LINSERT key BEFORE\|AFTER pivot value |在列表的元素前或者后插入元素 |示例:LINSERT list1 BEFORE value3 beforevalue3|
|6| LINDEX key index |通过索引获取列表中的元素 |示例:LINDEX list1 0|
|7 |LSET key index value |通过索引设置列表元素的值 |示例:LSET list1 0 hello|
|6 |LLEN key |获取列表长度 |示例:LLEN list1|
|9| LPOP key |移出并获取列表的第一个元素 |示例:LPOP list1|
|10| RPOP key |移除列表的最后一个元素,返回值为移除的元素。 |示例:RPOP list1|
|11| BRPOP key1 [key2 ] timeout |移出并获取列表的最后一个元素, 如果列表没有元素会 **阻塞 (B表示阻塞)** 列表直到等待超时或发现可弹出元素为止。 |示例:BRPOP list1 2000|
|12 |RPOPLPUSH source destination| 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 |示例:RPOPLPUSH list1 list2|
|13 |BRPOPLPUSH source destination timeout |从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |示例:BRPOPLPUSH list1 list2 2000|
|14 |LTRIM key start stop |对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。| 示例:LTRIM list1 0 2|
|15 |DEL key1 key2 |删除指定key的列表 |示例:DEL list2|
>```bash
>127.0.0.1:6379> lpush list1 1 2 3 4 5 # 右边是头
>(integer) 5
>127.0.0.1:6379> lrange list1 1 3 # 从右往左数
>1) "4"
>2) "3"
>3) "2"
>```
- rpushx 案例
>```bash
>127.0.0.1:6379> lpush zhd 1,2,3,4,5
>(integer) 1
>127.0.0.1:6379> lrange zhd 0 -1
>1) "1,2,3,4,5"
>127.0.0.1:6379> rpushx zhd 6
>(integer) 2
>127.0.0.1:6379> lrange zhd 0 -1
>1) "1,2,3,4,5"
>2) "6"
>```
>```
>127.0.0.1:6379> rpush zhd2 1,2,3,4,5
>(integer) 1
>127.0.0.1:6379> lrange zhd2 0 -1
>1) "1,2,3,4,5"
>127.0.0.1:6379> rpushx zhd2 6
>(integer) 2
>127.0.0.1:6379> lrange zhd2 0 -1
>1) "1,2,3,4,5"
>2) "6"
>```
- lindex 案例
>```bash
>127.0.0.1:6379> lpush zhd 1,2,3,4,5
>(integer) 1
>127.0.0.1:6379> rpushx zhd 6
>(integer) 2
>127.0.0.1:6379> lindex zhd 0
>"1,2,3,4,5"
>127.0.0.1:6379> lindex zhd 1
>"6"
>
>```
- lpop 案例
>```bash
>127.0.0.1:6379> llen zhd
>(integer) 2
>127.0.0.1:6379> lpop zhd
>"1,2,3,4,5"
>127.0.0.1:6379> llen zhd
>(integer) 1
>127.0.0.1:6379> lset zhd 0 1,2,3,4,5 # 设置第 0 个位置的元素,现在第 0 个元素为 6
>OK
>127.0.0.1:6379> lrange zhd 0 -1
>1) "1,2,3,4,5"
>127.0.0.1:6379> llen zhd
>(integer) 1
>```
## 2.5. 对 set 的操作
- Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据
- Redis 中**集合是通过哈希表实现的**,所以添加,删除,查找的复杂度都是 O(1)。
- 集合中最大的成员数为 2的32 - 1 (4294967295, 每个集合可存储40多亿个成员)。
|序号| 命令|描述 |示例|
|:-:|:-:|:-:|:-:|
|1 |SADD key member1 [member2] |向集合添加一个或多个成员 |示例:SADD set1 setvalue1 setvalue2|
|2 |SMEMBERS key |返回集合中的所有成员 |示例:SMEMBERS set1|
|3 |SCARD key |获取集合的成员数 |示例:SCARD set1|
|4 |SDIFF key1 [key2] |返回给定所有集合的差集 |示例: SADD set2 setvalue2 setvalue3,SDIFF set1 set2|
|5 |SDIFFSTORE destination key1 [key2] |返回给定所有集合的差集并存储在 destination 中 |示例:SDIFFSTORE set3 set1 set2|
|6 |SINTER key1 [key2] |返回给定所有集合的交集 |示例:SINTER set1 set2|
|7 |SINTERSTORE destination key1 [key2] |返回给定所有集合的交集并存储在 destination 中 |示例:SINTERSTORE set4 set1 set2|
|8 |SISMEMBER key member |判断 member 元素是否是集合 key 的成员 |示例:SISMEMBER set1 setvalue1|
|9 |SMOVE source destination member |将 member 元素从 source 集合移动到 destination 集合 |示例:SMOVE set1 set2 setvalue1|
|10 |SPOP key |移除并返回集合中的一个随机元素 |示例:SPOP set2|
|11 |SRANDMEMBER key [count] |返回集合中一个或多个随机数 |示例:SRANDMEMBER set2 2|
|12 |SREM key member1 [member2] |移除集合中一个或多个成员 |示例:SREM set2 setvalue1|
|13 |SUNION key1 [key2]] |返回所有给定集合的并集 |示例:SUNION set1 set2|
|14 |SUNIONSTORE destination key1 [key2] |所有给定集合的并集存储在 destination 集合中| 示例:SUNIONSTORE set5 set1 set2|
>```bash
>127.0.0.1:6379> sadd szhd 1 2 2 3 4 5
>(integer) 5
>127.0.0.1:6379> smembers szhd
>1) "1"
>2) "2"
>3) "3"
>4) "4"
>5) "5"
>127.0.0.1:6379> sadd szhd2 2 4
>(integer) 2
>127.0.0.1:6379> sdiff szhd szhd2
>1) "1"
>2) "3"
>3) "5"
>127.0.0.1:6379> sdiffstore szhd3 szhd szhd2
>(integer) 3
>127.0.0.1:6379> smembers szhd3
>1) "1"
>2) "3"
>3) "5"
>```
## 2.6. 对 ZSet 的操作(重要)
- Redis有序集合和集合一样也是string类型元素的集合,且不允许重复的成员
- 它用来保存需要排序的数据,例如排行榜,一个班的语文成绩,一个公司的员工工资,一个论坛的帖子等。
- 有序集合中,每个元素都带有score(权重),以此来对元素进行排序
- 它有三个元素:key、member和score。
- 以语文成绩为例,key是考试名称(期中考试、期末考试等),member是学生名字,score是成绩。
- 互联网,微博热搜,最热新闻,统计网站pv
|序号|命令|描述|案例|
|:-:|:-:|:-:|:-:|
|1 |ZADD key score1 member1 [score2 member2] |向有序集合添加一个或多个成员,或者更新已存在成员的分数| 向ZSet中添加页面的PV值,ZADD pv_zset 120 page1.html 100 page2.html 140 page3.html|
|2 |ZCARD key |获取有序集合的成员数 |获取所有的统计PV页面数量 ZCARD pv_zset|
|3 |ZCOUNT key min max |计算在有序集合中指定区间分数的成员数 |获取PV在120-140在之间的页面数量 ZCOUNT pv_zset 120 140|
|4 |ZINCRBY key increment member |有序集合中对指定成员的分数加上增量 increment |给page1.html的PV值+1, ZINCRBY pv_zset 1 page1.html|
|5 |ZINTERSTORE destination numkeys key [key ...] |计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中 |创建两个保存PV的ZSET: ZADD pv_zset1 10 page1.html 20 page2.html ZADD pv_zset2 5 page1.html 10 page2.html ZINTERSTORE pv_zset_result 2 pv_zset1 pv_zset2|
|6 |ZRANGE key start stop [WITHSCORES] |通过索引区间返回有序集合指定区间内的成员,两边都是闭区间,默认按权重升序排列 | 获取所有的元素,并可以返回每个key对一个的score,ZRANGE pv_zset_result 0 -1 WITHSCORES|
|7 |ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] |通过分数返回有序集合指定区间内的成员 |获取ZSET中120-140之间的所有元素,ZRANGEBYSCORE pv_zset 120 140|
|8 |ZREVRANGE key start stop [WITHSCORES] |返回有序集中指定区间内的成员,通过索引,分数从高到低| 按照PV降序获取页面 ,ZREVRANGE pv_zset 0 -1|
|9 |ZSCORE key member |返回有序集中,成员的分数值 |获取page3.html的分数值, ZSCORE pv_zset page3.html|
|10 |ZRANK key member |返回有序集合中指定成员的索引 |获取page1.html的pv排名(升序),ZRANK pv_zset page3.html|
|11 |ZREVRANK key member |返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |获取page2.html的pv排名(降序) ZREVRANK pv_zset page2.html|
|12 |ZREM key member [member ...] |移除有序集合中的一个或多个成员 |移除page1.html , ZREM pv_zset page1.html|
>```bash
>127.0.0.1:6379> ZADD pv_zset 120 page1.html 100 page2.html 140 page3.html
>(integer) 3
>127.0.0.1:6379> zcard pv_zset
>(integer) 3
>127.0.0.1:6379> zcount pv_zset 130 150 # 查看权重在 130 到 150 的成员数
>(integer) 1
>127.0.0.1:6379> zscore pv_zset page1.html # 获取成员分数值
>"120"
>127.0.0.1:6379> zrange pv_zset 0 -1
>1) "page2.html"
>2) "page1.html"
>3) "page3.html"
>127.0.0.1:6379> zrange pv_zset 0 -1 withscores # 返回带有权重的有序集合
>1) "page2.html"
>2) "100"
>3) "page1.html"
>4) "120"
>5) "page3.html"
>6) "140"
>127.0.0.1:6379> ZRANK pv_zset page3.html
>(integer) 2
>127.0.0.1:6379> zrevrange pv_zset 0 -1 withscores # 常用,效率很高,因为zset本身就是排好序的
>1) "page3.html"
>2) "140"
>3) "page1.html"
>4) "120"
>5) "page2.html"
>6) "100"
>```
## 2.7. 对位图 BitMap 的操作
- 计算机最小的存储单位是位 bit,Bitmaps 是针对位的操作的,相较于 String、Hash、Set 等存储方式更加节省空间
- Bitmaps不是一种数据结构,**操作是基于String结构的**,一个 String 最大可以存储 512 M,那么一个 Bitmaps 则可以设置 2^32 个位
- **Bitmaps单独提供了一套命令**,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。
- 可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1
- 数组的下标在 Bitmaps 中叫做偏移量 offset
- BitMaps 命令说明:将每个独立用户是否访问过网站存放在Bitmaps中
- 举例: 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id 。
>```
>unique:users:2022-04-05 0 1 0 0
>```
### 2.7.1. 设置值
>```bash
>SETBIT key offset value
>```
- setbit命令设置的vlaue只能是0或1两个值
- 置键的第offset个位的值(从0算起),假设现在有20个用户,uid=0,5,11,15,19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图所示
- 具体操作过程如下, unique:users:2022-04-05 代表 2022-04-05 这天的独立访问用户的 Bitmaps
>```bash
>setbit unique:users:2022-04-05 0 1
>setbit unique:users:2022-04-05 5 1
>setbit unique:users:2022-04-05 11 1
>setbit unique:users:2022-04-05 15 1
>setbit unique:users:2022-04-05 19 1
>```
- 问题:
- 很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户 id 和 Bitmaps 的偏移量对应势必会造成一定的浪费, **通常的做法是每次做 setbit 操作时将用户 id 减去这个指定数字。**
- 在第一次初始化 Bitmaps 时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。
>```
>10000000 10000005 10000011
>```
### 2.7.2. 获取值
- 基本语法
>```bash
>GETBIT key offset
>```
- 获取键的第offset位的值(从0开始算)
- 例:下面操作获取 id=8 的用户是否在 2022-04-05 这天访问过, 返回0说明没有访问过。
>```bash
>127.0.0.1:6379> getbit unique:users:2022-04-05 8
>(integer) 0
>127.0.0.1:6379> getbit unique:users:2022-04-05 5
>(integer) 1
>```
### 2.7.3. 获取Bitmaps指定范围值为1的个数
- 基本语法
>```bash
>BITCOUNT key [start end] # [] 表示可选
>```
- 例如,下面操作计算2022-04-05这天的独立访问用户数量
>```bash
>127.0.0.1:6379> bitcount unique:users:2022-04-05
>(integer) 5
>127.0.0.1:6379> bitcount unique:users:2022-04-05 1 7 # 限定范围
>(integer) 3
>```
### 2.7.4. Bitmap 间的运算
- 基本语法
>```bash
>BITOP operation destkey key [key, …]
>```
- bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果**保存在 destkey 中。**
- 需求:假设2022-04-04访问网站的userid=1, 2, 5, 9, 如下所示
>```bash
>setbit unique:users:2022-04-05 0 1
>setbit unique:users:2022-04-05 5 1
>setbit unique:users:2022-04-05 11 1
>setbit unique:users:2022-04-05 15 1
>setbit unique:users:2022-04-05 19 1
>setbit unique:users:2022-04-04 1 1
>setbit unique:users:2022-04-04 2 1
>setbit unique:users:2022-04-04 5 1
>setbit unique:users:2022-04-04 9 1
>```
- 例1:下面操作计算出2022-04-04和2022-04-05两天都访问过网站的用户数量
>```bash
>127.0.0.1:6379> bitop and unique:users:and:2022-04-04_05 unique:users:2022-04-04 unique:users:2022-04-05
>(integer) 3 # 这里存储的是运算后存储的集合的长度大小,并不是元素大小
>
>127.0.0.1:6379> bitcount unique:users:and:2022-04-04_05
>(integer) 1 # 只有一个数据, 说明只有一个用户在 4 号和 5 号登录
>```
- 例2:如果想算出 2022-04-04 和 2022-04-05 任意一天都访问过网站的用户数量(例如月活跃就是类似这种)
- 可以使用or求并集
>```bash
>127.0.0.1:6379> bitop or unique:users:or:2022-04-04_05 unique:users:2022-04-04 unique:users:2022-04-05
>(integer) 3
>
>127.0.0.1:6379> bitcount unique:users:or:2022-04-04_05
>(integer) 8 # 一共登录的用户数为 8
>```
## 2.8. 对 HyperLogLog 结构的操作
### 2.8.1. HyperLogLog 应用场景
- 应用场景
- HyperLogLog 常用于大数据量的统计,比如页面访问量统计或者用户访问量统计
- **HyperLogLog 可以理解为类似 Set 的结构,但是是用于统计大数据量的情况**
- 例如:
- 要统计一个页面的访问量(PV),可以直接用 redis 计数器或者直接存数据库都可以实现,
- 如果要统计一个页面的用户访问量(UV),一个用户一天内如果访问多次的话,也只能算一次
- 我们可以使用SET集合来做,因为**SET集合是有去重功能**的,key 存储页面对应的关键字,value 存储对应的 userid,这种方法是可行的。
- 但如果访问量较多,假如有几千万的访问量,这就麻烦了。为了统计访问量,**要频繁创建SET集合对象。**
- 这样的话 Redis 的压力就太大了
- 为了解决上述问题,Redis实现HyperLogLog算法,发明人 是Philippe Flajolet
>```bash
>127.0.0.1:6379> help @hyperloglog
>
> PFADD key element [element ...] # PF开头,就是发明人姓名缩写,夹带私货了属于是
> summary: Adds the specified elements to the specified HyperLogLog.
> since: 2.8.9
>
> PFCOUNT key [key ...]
> summary: Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).
> since: 2.8.9
>
> PFMERGE destkey sourcekey [sourcekey ...]
> summary: Merge N different HyperLogLogs into a single one.
> since: 2.8.9
>```
- 案例:来演示如何计算uv
>```bash
>127.0.0.1:6379> pfadd uv user1
>(integer) 1
>127.0.0.1:6379> keys uv
>1) "uv"
>127.0.0.1:6379> pfcount uv
>(integer) 1
>127.0.0.1:6379> pfadd uv user2 user3 user4 user5 user5 user6 user7 user8 user9 user10
>(integer) 1
>127.0.0.1:6379> pfcount uv
>(integer) 10
>```
- HyperLogLog算法一开始就是为了**大数据量的统计**而发明的,所以很适合那种数据量很大,然后又没要求不能有一点误差的计算
- HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%
- 不过这对于页面用户访问量是没影响的,因为这种统计可能是访问量非常巨大,但是又没必要做到绝对准确,访问量对准确率要求没那么高,但是性能存储方面要求就比较高了,而HyperLogLog正好符合这种要求,不会占用太多存储空间,同时性能不错
- 需求:假如两个页面很相近,现在想统计这两个页面的用户访问量
- 使用 pfmerge 将两个页面的数据合并
>```bash
>127.0.0.1:6379> pfadd page1 user1 user2 user3 user4 user5
>(integer) 1
>127.0.0.1:6379> pfadd page2 user1 user2 user3 user6 user7
>(integer) 1
>127.0.0.1:6379> pfmerge page1+page2 page1 page2
>OK
>127.0.0.1:6379> pfcount page1+page2
>(integer) 7
>```
### 2.8.2. HyperLogLog为什么适合做大量数据的统计
- HyperLogLog 的优点
- 在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的
- 就是为大量数据统计而设计的
- 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。
- 这和计算基数时,元素越多耗费内存就越多的 SET 形成鲜明对比。
- 因为 HyperLogLog 只会根据输入元素来计算基数 (**基数统计算法**),而 **不会储存输入元素本身**,所以 **HyperLogLog 不能像集合那样,返回输入的各个元素**
- 什么是基数
- 比如:数据集{1, 3, 5, 7, 5, 7, 8},那么这个数据集的基数集{1, 3, 5, 7, 8},基数(不重复元素)为 5。
- 基数估计就是在误差可接受的范围内,快速计算基数。
## 2.9. Redis Java API
### 2.9.1. 创建 Maven 工程
>```xml
>
>
> redis.clients
> jedis
> 2.9.0
>
>
> junit
> junit
> 4.12
> test
>
>
> org.testng
> testng
> 6.14.3
> test
>
>
>
>
>
> org.apache.maven.plugins
> maven-compiler-plugin
> 3.0
>
> 1.8
> 1.8
> UTF-8
>
>
>
>
>
>```
- 编写代码如下
>```java
>package org.zhd.redislearn;
>
>import java.util.Set;
>
>import org.junit.After;
>import org.junit.Before;
>import org.junit.Test;
>
>import redis.clients.jedis.Jedis;
>import redis.clients.jedis.JedisPool;
>import redis.clients.jedis.JedisPoolConfig;
>
>public class myredistest {
> JedisPool jedisPool;
> // 在测试之间创建
> @Before
> public void beforeTest() {
> // 创建一共 Jedis 连接池
> JedisPoolConfig config = new JedisPoolConfig();
> Jedis jedis;
> String host = "node01";
> int port = 6379;
> // 设置最大空闲连接
> config.setMaxIdle(10);
> // 最小空闲连接数
> config.setMinIdle(5);
> // 设置超时时间
> config.setMaxWaitMillis(3000);
> // 设置 JedisPool 的最大连接数
> config.setMaxTotal(50);
>
> jedisPool = new JedisPool(config,host,port);
> }
>
> @Test
> public void test() {
> // 从连接池拿一个连接
> jedis = jedisPool.getResource();
> Set res = jedis.keys("*");
> for (String key : res) {
> System.out.println(key);
> }
>
> }
>
> // 在测试之后关闭相关连接
> @After
> public void afterTest() {
> jedis.close();
> jedisPool.close();
> }
>}
>```
- 如果出现如下错误
>```
>Could not get a resource from the pool
>```
- 则是可能是由于新装 redis 没有改配置引起的,执行在 redis.conf 文件中执行以下操作
- 1. 将 bind 127.0.0.1 注释掉
- 2. 将 protected-mode 改为 no
- 执行结果如下
>```
>unique:users:or:2022-04-04_05
>uv
>szhd2
>pv_zset
>unique:users:xor:2022-04-04_05
>page1+page2
>page2
>page1
>szhd
>unique:users:2022-04-05
>unique:users:2022-04-04
>zhd
>unique:users:and:2022-04-04_05
>szhd3
>```
### 2.9.2. 操作 string 类型数据
- 需求
- 添加一个 string 类型数据,key为pv,用于保存pv的值,初始值为0
- 查询该key对应的数据
- 修改pv为1000
- 实现整形数据原子自增操作 +1
- 实现整形该数据原子自增操作 +1000
- 实现
>```java
>public class myredistest {
> Jedis jedis;
> JedisPool jedisPool;
> // 在测试之间创建
> @Before
> public void beforeTest() {
> // 创建一共 Jedis 连接池
> JedisPoolConfig config = new JedisPoolConfig();
> String host = "node01";
> int port = 6379;
> // 设置最大空闲连接
> config.setMaxIdle(10);
> // 最小空闲连接数
> config.setMinIdle(5);
> // 设置超时时间
> config.setMaxWaitMillis(3000);
> // 设置 JedisPool 的最大连接数
> config.setMaxTotal(50);
> jedisPool = new JedisPool(config,host,port);
> }
>
>
> @Test
> public void testString() {
> // 从连接池拿一个连接
> Jedis jedis = jedisPool.getResource();
> // 设置 pv 初始值
> jedis.set("pv", "0"); // 这里的 0 必须是字符串
> // 查询 pv 数据
> System.out.println("pv初始值:"+jedis.get("pv"));
> // 修改值为 1000
> jedis.set("pv","1000");
> System.out.println("修改值为 1000:"+jedis.get("pv"));
> // 实现整型数据原子自增加1
> Long res1 = jedis.incr("pv"); // 返回值就是自增后的值
> System.out.println("pv自增+1:"+res1);
> // 原子自增草走 + 1000
> Long res2 = jedis.incrBy("pv", 1000);
> System.out.println("pv自增1000:"+res2);
>
> }
>
>
> // 在测试之后关闭相关连接
> @After
> public void afterTest() {
> jedis.close();
> jedisPool.close();
> }
>}
>```
- 输出结果如下
>```
>pv初始值:0
>修改值为 1000:1000
>pv自增+1:1001
>pv自增1000:2001
>```
### 2.9.3. 操作 hash 类型数据
- 需求
- 往Hash结构中添加以下商品库存
- iphone11 => 10000
- macbookpro => 9000
- 获取Hash中所有的商品
- 新增3000个macbookpro库存
- 删除整个Hash的数据
>```java
>@Test
>public void testHash() {
> Jedis jedis = jedisPool.getResource();
> // 设置 hash 值
> jedis.hset("myhash","iphone11","10000");
> jedis.hset("myhash", "macbookpro", "9000");
> // 获取 hash 里面的所有商品
> System.out.println("所有商品:");
> Set goods = jedis.hkeys("myhash");
> goods.stream().forEach(System.out::println);
> // 自增 3000 个 macbookpro 库存
> Long res = jedis.hincrBy("myhash", "macbookpro", 3000);
> System.out.println("自增 3000 个 macbookpro:"+res);
> // 删除整个Hash的数据
> jedis.del("myhash");
>
>}
>```
### 2.9.4. 操作 list 类型数据
- 需求
- 向 list 的左边插入以下三个手机号码:18511310002、18912301233、18123123314
- 从右边移除一个手机号码
- 获取list所有的值
>```java
>@Test
>public void testList() {
> jedis = jedisPool.getResource();
> jedis.del("mylist");
> jedis.lpush("mylist", "18511310002");
> jedis.lpush("mylist", "18912301233");
> jedis.lpush("mylist", "18123123314");
> // 删除前
> System.out.println(jedis.lrange("mylist", 0, jedis.llen("mylist")));
> // 从右边移除一个手机号码
> jedis.rpop("mylist");
> System.out.println(jedis.lrange("mylist", 0, jedis.llen("mylist")));
>}
>```
- 输出结果
>```
>[18123123314, 18912301233, 18511310002] # 栈顶是 index = 0
>[18123123314, 18912301233] # 从左往右插入,所以栈底是最右边的
>```
### 2.9.5. 操作set类型的数据
- 需求
- 使用 set 来保存uv值,为了方便计算,将用户名保存到 uv 中。
- 往一个 set 中添加页面 page1 的 uv,用户 user1 访问一次该页面
- user2 访问一次该页面
- user1 再次访问一次该页面
- 最后获取 page1 的uv值
>```java
>@Test
>public void testSet() {
> jedis = jedisPool.getResource();
> jedis.del("uv");
> jedis.sadd("uv", "user1");
> jedis.sadd("uv", "user2");
> jedis.sadd("uv", "user1");
> // 获取 page1 的 uv 值
> System.out.println(jedis.scard("uv"));
>}
>```
- 得到结果为 2
# 3. Redis 持久化
- Redis 是内存数据库,所有数据都是保存在内存中的,内存中的数据极其容易丢失
- 因此 redis 需要将数据持久化,持久化有两种方式
- RDB(Redis DataBase):默认的持久化方式
- AOF
## 3.1. RDB 持久化方案
- Redis 会定期保存数据快照至一个 rdb 文件 (文件在硬盘中) 中,并在启动时自动加载 rdb 文件,回复之前保存的数据
- 可以在配置文件中配置 Redis 进行快照保存的时机
>```bash
>save [seconds] [changes] # 意为在 seconds 秒内如果发生了 changes 次数据修改,则进行一次 RDB 快照保存
>```
- 例如
>```bash
>save 60 100 # 会让 Redis 每 60 秒检查一次数据变更情况,如果发生了 100 次或以上的数据变更,则进行 RDB 快照保存
>save 600 500 # 可以配置多条 save 指令,让 Redis 执行多级的快照保存策略。在多条配置时,只要满足其中一条,即可触发保存
>```
- **SAVE 和 BGSAVE 命令**
- SAVE 和 BGSAVE 两个命令都会调用 rdbSave 函数
- SAVE 直接调用 rdbSave ,**阻塞 Redis 主进程**,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。
- **BGSAVE 则 fork 出一个子进程**,子进程负责调用 rdbSave ,从后台执行数据保存操作,并在保存完成之后向主进程发送信号,通知保存已完成。 **Redis 服务器在 BGSAVE 执行期间仍然可以继续处理客户端的请求**
- 其可用性要优于执行 SAVE 命令
- 但是在实际开发过程中,在配置文件中已经配置好了,Redis自己会执行保存的,无需手动执行命令。有那也是运维的事情
### 3.1.1. 优缺点
- **RDB 方案的优点**
- 对性能影响最小。
- Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
- 每次快照会生成一个**完整的数据快照文件**
- 可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段
- 使用RDB文件进行数据恢复比使用AOF要快很多。
- **RDB 方案缺点**
- 快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据
- save 60 100 ,会让 Redis 每 60 秒检查一次数据变更情况,如果发生了 100 次或以上的数据变更,则进行 RDB 快照保存
- **但如果 60 内只发生了 99 次修改,则不会进行保存**,因此会造成数据丢失
- 如果数据集非常大且 CPU 不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间,影响Redis对外提供服务的能力
### 3.1.2. RDB 配置
- 修改redis的配置文件
>```bash
>[zhd@node01 ~]$ whereis redis
>redis: /usr/lib64/redis /etc/redis.conf
>[zhd@node01 ~]$ sudo vim /etc/redis.conf
>```
- redis 的配置文件默认自带的存储机制。
- 表示每隔多少秒,有多少个key发生变化就生成一份 dump.rdb 文件,具体如下:
>```bash
>################################ SNAPSHOTTING ################################
>#
># Save the DB on disk:
>#
># save
>#
># Will save the DB if both the given number of seconds and the given
># number of write operations against the DB occurred.
>#
># In the example below the behaviour will be to save:
># after 900 sec (15 min) if at least 1 key changed
># after 300 sec (5 min) if at least 10 keys changed
># after 60 sec if at least 10000 keys changed
>#
># Note: you can disable saving completely by commenting out all "save" lines.
>#
># It is also possible to remove all the previously configured save
># points by adding a save directive with a single empty string argument
># like in the following example:
>
>save 900 1
>save 300 10
>save 60 10000
>```
- 配置完成后重启 redis 服务
- **每次生成新的 dump.rdb 都会覆盖掉之前的老的快照**
>```bash
>ps -ef | grep redis
>bin/redis-cli -h 192.168.200.131 shutdown
>bin/redis-server redis.conf
>```
## 3.2. AOF 持久化方案
- 介绍
- 采用 AOF 持久方式时,Redis 会把 **每一个写请求**都记录在一个**日志文件**中
- 在 Redis 重启时,会把 AOF 文件中记录的 **所有写操作顺序执行一遍**,确保数据恢复到最新
- 这有点像 [Mysql 的 binlog 数据恢复](https://gitee.com/HaiXiuDeDXianSheng/msql-learning-notes#9123-%E6%80%BB%E7%BB%93),会将指定范围内的 sql 从头到尾重新执行一遍
- 开启 AOF
- **AOF 默认是关闭的**,如果要开启,可在配置文件中进行如下配置
>```
> # 第594行
> appendonly yes
> ```
### 3.2.1. 配置 AOF
- AOF 提供了三种 fsync 配置,通过 appendfsync 指定
- **appendfsync no**
- 不进行 fsync,将 flush 文件的时机交给 OS 决定,速度最快
- **appendfsync always**
- 每写入一条日志就进行一次 fsync 操作,数据安全性最高,但速度最快
- **appendfsync everysec**
- 折中的做法,交给后台线程每秒 fsync 一次
### 3.2.2. AOF rewrite
- 随着 AOF 不断记录写操作日志,所有的写操作都会被记录
- 必定会出现无用的日志,使得 AOF 过大,造成数据恢复时间过长
- AOF rewrite 功能,可以重写 AOF 文件,只保留能够把数据恢复到最新准阿根廷的最小写操作集
- AOF rewrite 可以通过 BGREWRITEAOF 命令触发,也可以配置 Redis 定期自动进行:
>```
>auto-aof-rewrite-percentage 100 # 增长 100% 后开启重写
>auto-aof-rewrite-min-size 64mb
>```
- auto-aof-rewrite-percentage
- Redis 在每次 AOF rewrite 时,会记录完成rewrite后的 AOF 日志大小,**当 AOF 日志大小在该基础上增长了100%后,自动进行 AOF rewrite**, 32m--->10万次操作-->64M-->AOF rewrite--->40M-->100万次操作-->80M-->AOF rewrite-->50M-->>100m
- auto-aof-rewrite-min-size
- 最开始的 AOF 文件必须要触发这个文件才触发,后面的每次重写就不会根据这个变量了。
- 该变量仅初始化启动Redis有效
### 3.2.3. AOF 优缺点
- 优点
- 最安全,在启用 appendfsync 为 always 时,任何已写入的数据都不会丢失,使用在启用 appendfsync everysec 也至多只会丢失1秒的数据
- AOF 文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用 redis-check-aof 工具轻松修复
- AOF 文件易读,可修改,在进行某些错误的数据清除操作后,只要 AOF 文件没有 rewrite,就可以把 AOF 文件备份出来,把错误的命令删除,然后恢复数据
- 缺点
- AOF 文件通常比 RDB 文件更大,性能消耗比 RDB 高,数据恢复速度比 RDB 慢
- Redis 的数据持久化工作本身就会带来延迟,需要根据数据的安全级别和性能要求制定合理的持久化策略:
- AOF + fsync always 的设置虽然能够绝对确保数据安全,但每个操作都会触发一次 fsync,会对 Redis 的性能有比较明显的影响
- AOF + fsync every second 是比较好的折中方案,每秒 fsync 一次
- AOF + fsync never 会提供 AOF 持久化方案下的最优性能
- 使用 RDB 持久化通常会提供比使用AOF更高的性能,但需要注意 RDB 的策略配置
## 3.3. RDB VS AOF
- RDB 类似于 Mysql 的 redo log 和 undo log,记录的是实打实的数据
- 其实和 redo log 和 undo log 也不太像,因为它们记录的也是数据在物理存储方面的变化。
- 而 RDB 是每次保存当前版本数据,覆盖上一版本
- AOF 类似于 mysql 的 binlog,记录的是操作日志,记录的是过程
- 每一次 RDB 快照和 AOF Rewrite 都需要 Redis 主进程进行 fork 操作
- fork 操作本身可能会产生较高的耗时,与 CPU 和 Redis 占用的内存大小有关
- 一个可行的策略是,最后的从机上,rdb、aof都开启
# 4. Redis 事务
- Redis 事务的本质是**一组命令的合集**,就是执行队列中的一组命令
- 事务支持一次执行多个命令,一个事务中所有命令都会被序列化
- 在事务执行过程中,会按照顺序穿行化执行队列中的命令
- 客户端提交的命令不会插入到事务执行命令序列中
- **总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令**
- **Redis不保证原子性**
- Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
- **Redis事务没有隔离级别的概念**
- 批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到
- 一个事务从开始到执行会经历以下三个阶段:
- 第一阶段:开始事务
- 第二阶段:命令入队
- 第三阶段:执行事务
## 4.1. 事务相关命令
- MULTI
- 开启事务,redis 会将后续的命令逐个放入队列中,然后 EXEC 命令来原子化执行这个命令队列
- EXEC
- 执行事务中的所有操作命令
- DISCARD
- 取消事务,放弃执行事务块中的所有命令
- WATCH
- 监视一个或多个 key,如果事务在执行前,这个 key (或多个 key) 被其他命令修改,则事务被终端,不会执行事务中的任何命令
- UNWATCH
- 取消 WATCH 对所有 key 的监视
## 4.2. Redis 事务演示
### 4.2.1. 事务成功演示
- MULTI 开始一个事务:给k1、k2分别赋值,在事务中修改k1、k2,执行事务后,查看k1、k2值都被修改。
>```bash
>[zhd@node01 ~]$ redis-cli
>127.0.0.1:6379> set key1 v1
>OK
>127.0.0.1:6379> set key2 v2
>OK
>127.0.0.1:6379> multi
>OK
>127.0.0.1:6379> set key1 11
>QUEUED
>127.0.0.1:6379> set key2 22
>QUEUED
>127.0.0.1:6379> exec
>1) OK
>2) OK
>127.0.0.1:6379> get key1
>"11"
>127.0.0.1:6379> get key2
>"22"
>```
### 4.2.2. 事务失败演示
- 语法错误 (编译器异常)
>```bash
>192.168.200.131:6379> set key1 v1
>OK
>192.168.200.131:6379> set key2 v2
>OK
>192.168.200.131:6379> multi
>OK
>192.168.200.131:6379(TX)> set key1 11
>QUEUED
>192.168.200.131:6379(TX)> sets key2 22
>(error) ERR unknown command `sets`, with args beginning with: `key2`, `22`,
>192.168.200.131:6379(TX)> exec
>(error) EXECABORT Transaction discarded because of previous errors.
>192.168.200.131:6379> get key1 # 事务回滚
>"v1"
>192.168.200.131:6379> get key2
>"v2"
>```
- 运行时错误
>```bash
>192.168.200.131:6379> set key1 v1
>OK
>192.168.200.131:6379> set key2 v2
>OK
>192.168.200.131:6379> multi
>OK
>192.168.200.131:6379(TX)> set key1 11
>QUEUED
>192.168.200.131:6379(TX)> lpush key2 22 # 将 key2 当作列表
>QUEUED
>192.168.200.131:6379(TX)> exec
>1) OK
>2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
>192.168.200.131:6379> get key1 # 不保证事务原子性(原子性,即要么全部成功,要么全部失败)
>"11"
>192.168.200.131:6379> get key2
>"v2"
>```
- DISCARD 取消事务
- 相当于 mysql 的 rollback 命令
>```bash
>192.168.200.131:6379> set key1 v1
>OK
>192.168.200.131:6379> set key2 v2
>OK
>192.168.200.131:6379> multi
>OK
>192.168.200.131:6379> set key1 v1
>QUEUED
>192.168.200.131:6379> set key2 v2
>QUEUED
>192.168.200.131:6379> discard
>OK
>192.168.200.131:6379> get key1
>"v1"
>192.168.200.131:6379> get key2
>"v2"
>```
## 4.3. 为什么 Redis 不像 Mysql 一样支持事务回滚?
- 多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的
- Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的
- Redis之所以保持这样简易的事务,**完全是为了保证高并发下的核心问题,性能**
- 用 Redis,就是为了图快
- 而事务本身很耗费性能,速度慢,如果为了图事务,就不用 Redis 了,mysql 不香嘛?
# 5. 数据删除与淘汰策略
## 5.1. 过期数据
### 5.1.1. Redis中的数据特征
- Redis 是一种内存级数据库,所有数据均放在内存上,内存中的数据可以通过 TTL 命令获取其状态。
- **TTL 返回的值有三种状态**
- **正数** : 代表该数据在内存中还能存活的时间
- **-1** : 永久有效的数据
- **-2** : 已经过期的数据,或被删除的数据,或未定义的数据
- 删除策略就是针对已国企的数据的处理策略
- 已过期的数据是真的就立刻删除嘛?不一定
### 5.1.2. 时效性数据的存储结构
- 过期数据是一块独立的存储空间,Hash 结构,**field 是内存地址,value 是过期时间**,保存了所有 key 的过期描述
- key-value 是一块地址,**过期时间是又开辟了一块地址进行存储**
- 在最终进行过期处理的时候,对该空间的数据进行检测,当时间到期之后通过 field 找到内存该地址处的数据,然后进行相关操作
## 5.2. 数据删除策略
### 5.2.1. 数据删除策略的目标
- 在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体的 redis 性能下降,甚至引发服务宕机或内存泄露
- 删除策略有如下
- 定时删除
- 惰性删除
- 定期删除
### 5.2.2. 定时删除
- 创建一个定时器,当 key 设置有过期时间,且过期时间到达时,由定时器任务立即执行对 key 的删除操作
- 优点: 节约内存,到时就删除,快速释放不必要的内存占用
- 缺点: CPU 压力很大,无论 CPU 此时负载量多高,均占用 CPU, 会影响 Redis 服务器响应时间和指令吞吐量
- 总结: 用处理器性能换取存储空间
### 5.2.3. 惰性删除
- 数据到达过期时间,不做处理,等下次访问该数据时,我们需要进行判断
- **如果数据未过期,则返回数据**
- **如果数据已过期,则删除,返回不存在**
- 优点: 节约 CPU 性能, 发现必须删除的时候才删除
- 缺点: 内存压力很大,长期占用内存的数据
- 总结: 用存储空间换取处理器性能
### 5.2.4. 定期删除
- 这是一种“定时删除”和“惰性删除”的折中方案
- **Redis 定期删除方案**
- Redis启动服务器初始化时,读取配置server.hz的值,默认为10
- 每秒钟执行server.hz次 serverCron()-------->databasesCron()--------->activeExpireCycle()
- **activeExpireCycle()** 对每个 expires[*] 库 (从 0 号库到 15 号库) 逐一进行检测,每次执行耗时:250ms/server.hz
- 对某个expires[*]检测时,随机挑选W个key检测
- 如果 key 超时,则删除 key
- 如果一轮中删除的 key 的数据量 > W*25%, 则循环对该 expires[\*] 的检测过程
- 如果一轮删除的 key 的数量 ≤ W*25%, 则检查下一个 expires[\*] 库
- W取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 属性值
- 参数 current_db 用于记录 activeExpireCycle() 进入哪个expires[*] 执行
- 如果 activeExpireCycle() 执行时间到期,下次从 current_db 继续向下执行
- **定期删除,就是周期性轮询 redis 库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度**
- 特点:
- CPU 性能占用设置有峰值,检测频度可自定义设置
- 内存压力不是很大,长期占用内存的冷数据会被持续清理
- 总结
- 周期性抽查存储空间 (随机抽查,重点抽查)
## 5.3. 数据淘汰策略-逐出算法 (面试重点)
### 5.3.1. 淘汰策略概述
- 淘汰策略, 讨论的就是 内存满了之后,再有新数据来存,怎么办.
- 在执行每一个命令前,会调用 **freeMemoryIfNeeded()** 检测内存是否充足, 如果内存不满足新加入数据的最低存储要求, redis 要临时删除一些数据为当前指令清理存储空间. 清理数据的策略为 **逐出算法**
- 注意:逐出数据的过程**不是 100% 能够清理出足够的可使用的内存空间**
- 如果不成功则反复执行。当对所有数据尝试完毕,如不能达到内存清理的要求,将出现错误信息如下
>```bash
>(error) OOM command not allowed when used memory >'maxmemory'
>```
### 5.3.2. 策略配置
- 影响数据淘汰策略的相关配置如下
#### 5.3.2.1. 最大可用内存
- 最大可用内存,即占用物理内存的比例, 默认为 0, 表示不限制. 生产环境中根据需求设定, 通常设置 50% 以上.
>```yaml
>maxmemory ?mb
>```
#### 5.3.2.2. 每次选取待删除数据的个数
- 采用随机获取数据的方式作为待检测删除输出据
>```yaml
>maxmemory-samples count
>```
#### 5.3.2.3. 对数据进行删除的选择策略
>```yaml
>maxmemory-policy policy_name
>```
- 数据删除的策略 policy 一共有 3 类 8 种
##### 5.3.2.3.1. 第一类: 检测易失数据
- 检测易失数据(可能会过期的数据集server.db[i].expires) **同一个库** 中的设置
>```yaml
>volatile-lru:挑选最近最少使用的数据淘汰 least recently used
>volatile-lfu:挑选最近使用次数最少的数据淘汰 least frequently used
>volatile-ttl:挑选将要过期的数据淘汰
>volatile-random:任意选择数据淘汰
>```
##### 5.3.2.3.2. 第二类: 检测全库数据
- 检测全库数据 (所有数据集server.db[i].dict )
>```yaml
>allkeys-lru:挑选最近最少使用的数据淘汰
>allkeys-lfu::挑选最近使用次数最少的数据淘汰
>allkeys-random:任意选择数据淘汰,相当于随机
>```
##### 5.3.2.3.3. 放弃数据驱逐
>```yaml
>no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory)
>```
- 注意:这些策略是配置到哪个属性上?怎么配置?如下所示
>```yaml
>maxmemory-policy volatile-lru
>```
### 5.3.3. 淘汰策略配置依据
- 使用INFO命令输出监控信息,查询缓存 hit 和 miss 的次数,根据业务需求调优Redis配置
# 6. Redis主从复制架构
## 6.1. 主从复制简介
- 互联网应用的三高架构
- 高并发
- 应用要提供某一业务要能支持很多客户端同时访问的能力,我们称为并发,高并发意思就很明确了
- 高性能
- 性能带给我们最直观的感受就是:速度快,时间短
- 高可用
- 可用性: 一年中应用服务正常运行的时间占全年时间的百分比
### 6.1.1. 主从复制概念
- 为了避免单点Redis服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服务器上,连接在一起,并保证数据是同步的。即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现Redis的高可用,同时实现数据冗余备份。
- 概念:主从复制即将master中的数据即时、有效的复制到slave中
- 特征:一个master可以拥有多个slave,一个slave只对应一个master
- 职责:master和slave各自的职责不一样
- master
- 写数据
- 执行写操作时,将出现变化的数据自动同步到slave
- 读数据(可忽略)
- slave
- 读数据
- 写数据(禁止)
### 6.1.2. 主从复制的作用
- 读写分离: master 写,slave 写,提高服务器的读写负载能力
- 负载均衡: 基于主从结构,配合读写分离,由 slave 分担 master 负载,并根据需求的变化,改变 slave 的数量,通过多个从节点分担数据读取负载,大大提高 Redis 服务器并发量和数据吞吐量
- 故障恢复: 当 master 出现问题时,由 slave 提供服务,实现快速的故障恢复
- 数据冗余: 实现数据热备份,是持久化之外的一种数据冗余方式
- 高可用基石: 基于主从复制,构建哨兵模式与集群,实现 Redis 高可用方案
## 6.2. 主从复制工作流程 (三个阶段)
- 主要分为三个阶段
- 建立连接阶段 (即准备阶段)
- 数据同步阶段
- 命令传播阶段 (反复同步阶段)
### 6.2.1. 阶段一: 建立连接
- 建立 salve 到 master 的连接,使 master 能够识别 slave,并保存 slave 端口号,流程如下
- 步骤 1 : 设置 master 的地址和端口,保存 master 信息
- 步骤 2 : 建立 socket 连接
- 步骤 3 : 发送 ping 命令 (定时任务)
- 步骤 4 : 身份验证
- 步骤 5 : 发送 slave 端口信息
- 经过以上步骤,主从连接成功
#### 6.2.1.1. master 和 slave 互联
- 接下来就要通过某种方式将 master 和 slave 连接到一起
- 方式一 : 客户端发送命令
>```bash
>slaveof masterip masterport
>```
- 方式二 : 启动服务器参数
>```
>redis-server --slaveof masterip masterport
>```
- 方式三: 服务器配置 (**主流方式**)
>```
>slaveof masterip masterport
>```
- slave 系统信息
>```
>master_link_down_since_seconds
>masterhost & masterport
>```
- master 系统信息
>```
>uslave_listening_port(多个)
>```
#### 6.2.1.2. 主从断开连接
- 断开 slave 与 master 的连接,slave 断开连接后,不会删除已有数据,只是不再接收 master 发送的数据
>```bash
>slaveof no one
>```
#### 6.2.1.3. 授权访问
- master 客户端发送命令设置密码
>```bash
>requirepass password
>```
- master 配置文件设置密码
>```yaml
>config set requirepass password
>config get requirepass
>```
- slave 客户端发送命令设置密码
>```
>auth password
>```
- slave 配置文件设置密码
>```
>masterauth password
>```
- slave 启动服务器设置密码
>```bash
>redis-server -a password
>```
### 6.2.2. 阶段二: 数据同步
- 在 slave 初次连接 master 后,复制 master 中的所有数据到 slave 中
- 将 slave 的数据库状态更新成 master 当前数据库状态
- 同步过程如下
- 步骤 1: 请求同步数据
- 全量数据同步阶段
- 步骤 2: 创建 RDB 同步数据
- 步骤 3: 恢复 RDB 同步数据
- 部分数据同步阶段
- 步骤 4: 请求部分同步数据
- 步骤 5: 恢复部分同步数据
- **AOF 数据持久化,就是把命令从头到尾执行一遍**
#### 6.2.2.1. 数据同步阶段 master 说明
- 如果 master 数据量巨大,数据同步阶段应该避开流量高峰期,避免造成 master 阻塞,影响正常业务执行
- 通常在 0 点执行
- 复制缓冲区大小设置不合理,会导致数据溢出,如进行全量复制周期太长,进行部分复制时发现数据已经存在丢失的情况,必须进行第二次全量复制,致使 slave 陷入死循环状态。
>```yaml
>slave-serve-stale-data yes|no
>```
- master 单机内存占用主机内存的比例不应过大,建议使用 50%-70% 的内存,留下 30%-50% 的内存用于执行 bgsave 命令和创建复制缓冲区
#### 6.2.2.2. 数据同步阶段 slave 说明
- 为避免 slave 进行全量复制、部分复制时服务器响应阻塞,或者数据不同步,**建议关闭此期间从节点的对外服务**
>```
>slave-serve-stale-data yes|no
>```
- 数据同步阶段,master 发送给 slave 信息可以理解为 master 是 slave 的一个客户端,主动向 slave 发送命令
- **多个 slave 同时对 master 请求数据同步**,master 发送的 RDB 文件增多,会对带宽造成巨大冲击
- 如果 master 带宽不足,数据同步需要根据业务需求,**适量错峰**
- slave 过多时,建议调整拓扑结构
- 由一主多从结构变为树状结构,中间节点既是 Master 也是 slave
- 注意使用树状结构时,由于层级深度,导致深度越高的 slave 与最顶层 master 间数据同步延迟较大,数据一致性变差,谨慎选择
- 从节点2 的从节点,由 从节点2 给它同步数据
- **生产环境**
- 一开始就要想好 Redis 架构,一开始就把 N 主 M 从的 Redis 服务都启动起来
### 6.2.3. 阶段三: 命令传播
- 当 master 数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的状态,同步的动作称为**命令传播**
- master将接收到的数据变更命令发送给slave,slave接收命令后执行命令
#### 6.2.3.1. 命令传播阶段的部分复制
- 命令传播阶段出现了断网现象
- 网络闪断闪连: 忽略
- 短时间网络中断: 部分复制
- 长时间网络中断: 全量复制
- 部分复制的三个核心要素: **服务器运行 ID (run id)、主服务器的复制积压缓冲区、主从服务器的复制偏移量**
##### 6.2.3.1.1. 服务器运行ID(runid)
- 概念:服务器运行ID是每一台服务器每次运行的身份识别码,一台服务器多次运行可以生成多个运行id
- 组成:运行id由40位字符组成,是一个随机的十六进制字符
- 例如:fdc9ff13b9bbaab28db42b3d50f852bb5e3fcdce
- 作用:运行id被用于在服务器间进行传输,识别身份
- 如果想两次操作均对同一台服务器进行,必须每次操作携带对应的运行id,用于对方识别
- 实现方式:运行id在每台服务器启动时自动生成的,master在首次连接slave时,会将自己的运行ID发送给slave,slave保存此ID
- 通过info Server命令,可以查看节点的runid
##### 6.2.3.1.2. 复制缓冲区
- 概念:复制缓冲区,又名复制积压缓冲区,是一个先进先出(FIFO)的队列,用于存储服务器执行过的命令,每次传播命令,master都会将传播的命令记录下来,并存储在复制缓冲区
- 复制缓冲区默认数据存储空间大小是1M
- 当入队元素的数量大于队列长度时,最先入队的元素会被弹出,而新元素会被放入队列
- 作用:用于保存master收到的所有指令(仅影响数据变更的指令,例如set,select)
- 数据来源:当master接收到主客户端的指令时,除了将指令执行,会将该指令存储到缓冲区中
- **偏移量 offset** (字节数组的下标)
- 概念: 一个数字,描述复制缓冲区中的指令字节位置
- 分类
- master复制偏移量:记录发送给所有slave的指令字节对应的位置(多个)
- slave复制偏移量:记录slave接收master发送过来的指令字节对应的位置(一个)
- 作用:同步信息,比对master与slave的差异,当slave断线后,恢复数据使用
- 数据来源:
- master端:发送一次记录一次
- slave端:接收一次记录一次
- 字节值
- 通过offset区分不同的slave当前数据传播的差异
- master记录已发送的信息对应的offset
- slave记录已接收的信息对应的offset
### 6.2.4. 流程更新 (全量复制/部分复制)
### 6.2.5. 心跳机制
- 什么是心跳机制
- 进入命令传播阶段,master 与 slave 间需要进行信息交换,心跳机制需要进行维护,实现双方连接保持在线
- **master 心跳**
- 内部指令: PING
- 周期: 由 repl-ping-slave-period 决定,默认 10 秒
- 作用: 判断 slave 是否在线
- 查询: INFO replication 获取 slave 最后依次连接时间间隔,lag 项维持在 0 或 1 视为正常
- **slave 心跳**
- 内部指令: REPLCONF ACK
- 周期: 1 秒
- 作用1: 汇报 slave 自己的复制偏移量,获取最新的数据变更指令
- 作用2: 判断 master 是否在线
- 心跳阶段注意事项
- 当 slave 多数掉线,或延迟过高时,master 为保障数据稳定性,将拒绝所有信息同步
- slave数量少于2个,或者所有slave的延迟都大于等于8秒时,强制关闭master写功能,停止数据同步
>```
>min-slaves-to-write 2
>min-slaves-max-lag 8
>```
- slave数量由slave发送REPLCONF ACK命令做确认
- slave延迟由slave发送REPLCONF ACK命令做确认
- **完整的主从复制流程**
## 6.3. 搭建主从架构
- 详见: [主从架构搭建参考资料](https://www.ydlclass.com/doc21xnv/java/third/framework/4%E3%80%81redis/#_3%E3%80%81%E6%90%AD%E5%BB%BA%E4%B8%BB%E4%BB%8E%E6%9E%B6%E6%9E%84) 和 [视频教学](https://www.bilibili.com/video/BV1qq4y1x79a?p=41&spm_id_from=pageDriver&vd_source=ef3b6ea9fe0263a9b46388ed18875178)
- 真实生产环境主从部署在不同物理地方
- 注意在虚拟机部署时,先要将 slave 上的 appendonly 文件和 dump.rdb 文件删除
## 6.4. 主从架构常见问题 (架构师须知)
### 6.4.1. 频繁全量复制
- 1. 伴随着系统的运行,master的数据量会越来越大,**一旦master重启,runid将发生变化,会导致全部slave的全量复制操作**
- **内部优化调整方案**
- master内部创建 master_replid 变量,使用 runid 相同的策略生成,长度41位,并发送给所有slave
- 在master关闭时执行命令 shutdown save,进行 RDB 持久化,将 runid 与offset 保存到 RDB 文件中
>```
>repl-id repl-offset
>通过redis-check-rdb命令可以查看该信息
>```
- master 重启后加载 RDB 文件,恢复数据,重启后,将 RDB 文件中保存的 repl-id 与 repl-offset 加载到内存中
- 作用:本机保存上次 runid,重启后恢复该值,使所有 slave 认为还是之前的 master
>```
>master_repl_id=repl master_repl_offset =repl-offset
>通过info命令可以查看该信息
>```
- 2. 网络环境不佳,出现网络中断,slave不提供服务, 导致全量复制
- 问题原因:复制缓冲区过小,断网后 slave 的 offset 越界 (主机明明发送了 21W 条数据了,但从机只同步了 10W 条),触发全量复制
- 最终结果:slave 反复进行全量复制
- **解决方案:修改复制缓冲区大小**
>```
>repl-backlog-size 20mb
>```
- 建议设置如下:
- 1. 测算从master到slave的重连平均时长second 10s
- 2. 获取master平均每秒产生写命令数据总量write_size_per_second 10w
- 3. 最优复制缓冲区空间 = 2 * second * write_size_per_second (与公司实际有关)
- 例如 10w*10b 20mb
### 6.4.2. 频繁的网络中断
- ***问题现象:master的CPU占用过高 或 slave频繁断开连接**
- 问题原因
>```
>slave每1秒发送REPLCONFACK命令到master
>
>当slave接到了慢查询时(keys * ,hgetall等),会大量占用CPU性能
>
>master每1秒调用复制定时函数replicationCron(),比对slave发现长时间没有进行响应
>```
- 最终结果:master各种资源(输出缓冲区、带宽、连接等)被严重占用
- 解决方案:通过设置合理的超时时间,确认是否释放slave
>```
>repl-timeout seconds
>```
- 该参数定义了超时时间的阈值 (默认 60s),超过该值,室放 slave
- **问题现象: slave 与 master 连接断开**
- 问题原因
>```
>master发送ping指令频度较低
>master设定超时时间较短
>ping指令在网络中存在丢包
>```
- 解决方案: 提高 Ping 指令发送的频度
>```
>repl-ping-slave-period seconds
>```
- 超时时间 repl-time 的时间至少是 ping 指令频度的 5-10 倍,否则slave很容易判定超时
### 6.4.3. 数据不一致
- 问题现象: 多个 slave 获取相同数据不同步
- 问题原因: 网络信息不同步,数据发送有延迟
- 解决方案
>```
>优化主从间的网络环境,通常放置在同一个机房部署,如使用阿里云等云服务器时要注意此现象拉专线。
>vpn。4m 10万 2万/年
>监控主从节点延迟(通过offset)判断,如果slave延迟过大,暂时屏蔽程序对该slave的数据访问
>```
>```
>slave-serve-stale-data yes|no
>```
- 开启后仅响应info、slaveof等少数命令(慎用,除非对数据一致性要求很高
# 7. Redis 的 Sentinel 架构
## 7.1. 哨兵模式简介
### 7.1.1. 哨兵概念
- 已经有了主动架构,为什么还有哨兵?首先我们来看一个业务场景
- 如果 redis 的 master 宕机了,此时应该怎么办?
- 此时我们可能需要从一堆 slave 中重新选举出一个新的 master,那这个操作是怎么样的?有什么问题?
- 要实现上述功能,我们就需要 redis 的哨兵
- **哨兵**
- 哨兵 (Sentinel) 是一个分布式系统,用于对主从结构中的每台服务器都进行**监控**,当出现故障时通过**投票机制**选择新的 master 并将所有 slave 连接到新的 master
- 哨兵的作用
- 监控: 监控 master 和 slave
- 不断检查 master 和 slave 是否正常运行: master 存活监测、master 与 slave 运行情况检测
- 通知 (提醒): 当被监控的服务器出现问题时,向其他 (哨兵间、客户端间) 发送通知
- 自动故障转移: 断开 master 与 slave 的连接,选择一个 slave 作为新的 master,并告知客户端新的服务器地址
- 注意: 哨兵也是一台 redis 服务器,只是不提供数据相关服务,通常哨兵的数量为单数 (**用于投票**)
- Sentinel 的作用类似于 Zookeeper
## 7.2. 启动哨兵模式
- 配置哨兵 (配置相同,端口不同),参考 sentinel.conf 文件
- 真实生产环境中 ip 不一样
- 1. 设置端口号
- 2. 设置哨兵监听的主服务器信息, sentinel_number表示参与投票的哨兵数量
>```
>sentinel monitor master_name master_host master_port sentinel_number
>```
- 3. 设置判定服务器宕机时长,该设置控制是否进行主从切换
>```
>sentinel down-after-milliseconds master_name million_seconds
>```
- 4. 设置故障切换的最大超时时长
>```
>sentinel failover-timeout master_name million_seconds
>```
- 5. 设置主从切换后,同时进行数据同步的slave数量,数值越大,要求网络资源越高,数值越小,同步时间越长
>```
>sentinel parallel-syncs master_name sync_slave_number
>```
- 6. 启动哨兵
>```
>redis-sentinel filename
>```
## 7.3. 搭建哨兵模式
- 搭建哨兵模式详见 : [文档资料](https://www.ydlclass.com/doc21xnv/java/third/framework/4%E3%80%81redis/#_3%E3%80%81%E6%90%AD%E5%BB%BA%E5%93%A8%E5%85%B5) 和 [视频资料](https://www.bilibili.com/video/BV1qq4y1x79a?p=44&spm_id_from=pageDriver&vd_source=ef3b6ea9fe0263a9b46388ed18875178)
- 基本只需要修改 sentinel.conf 文件即可
## 7.4. java API 连接 Sentinel
### 7.4.1. 原生java代码
- 1. 在 创建一个新的类 ReidsSentinelTest
- 2. 构建JedisPoolConfig配置对象
- 3. 创建一个HashSet,用来保存哨兵节点配置信息(记得一定要写端口号)
- 4. 构建JedisSentinelPool连接池
- 5. 使用sentinelPool连接池获取连接
>```java
>package com.ydlclass.redis;
>
>import org.testng.annotations.AfterTest;
>import org.testng.annotations.BeforeTest;
>import org.testng.annotations.Test;
>import redis.clients.jedis.Jedis;
>import redis.clients.jedis.JedisPoolConfig;
>import redis.clients.jedis.JedisSentinelPool;
>
>import java.util.HashSet;
>import java.util.Set;
>
>/**
> * @Created by IT李老师
> * 公主号 “IT李哥交朋友”
> * 个人微 itlils
> */
>public class ReidsSentinelTest {
> JedisSentinelPool jedisSentinelPool;
>
> //1. 在 创建一个新的类 ReidsSentinelTest
> //2. 构建JedisPoolConfig配置对象
> //3. 创建一个HashSet,用来保存哨兵节点配置信息(记得一定要写端口号)
> //4. 构建JedisSentinelPool连接池
> //5. 使用sentinelPool连接池获取连接
> @BeforeTest
> public void beforeTest(){
> //创建jedis连接池
> JedisPoolConfig config=new JedisPoolConfig();
> //最大空闲连接
> config.setMaxIdle(10);
> //最小空闲连接
> config.setMinIdle(5);
> //最大空闲时间
> config.setMaxWaitMillis(3000);
> //最大连接数
> config.setMaxTotal(50);
>
>
> Set sentinels=new HashSet<>();
> sentinels.add("192.168.200.131:26379");
> sentinels.add("192.168.200.131:26380");
> sentinels.add("192.168.200.131:26381");
>
> jedisSentinelPool= new JedisSentinelPool("mymaster",sentinels,config);
> }
>
> @Test
> public void keysTest(){
> Jedis jedis = jedisSentinelPool.getResource(); // 直接从 JedisSentinelPool 中获取 Jedis 资源,而不是从 JedisPool 中获取相关资源
> Set keys = jedis.keys("*");
> for (String key : keys) {
> System.out.println(key);
> }
> }
>
>
> @AfterTest
> public void afterTest(){
> jedisSentinelPool.close();
> }
>
>}
>```
### 7.4.2. spring boot方式连接
- 没学过 spring boot,有待补充
## 7.5. 哨兵工作原理 (面试)
- 哨兵在进行主从切换经历三个阶段
- 监控
- 通知
- 故障转移
### 7.5.1. 监控
- 用于同步各个节点的状态信息
- 获取各个 sentinel 的状态 (是否在线)
- 获取 master 的状态
>```
>master属性
> prunid
> prole:master
>各个slave的详细信息
>```
- 获取所有 slave 的状态 (根据 master 中的 slave 信息)
>```
>slave属性
> prunid
> prole:slave
> pmaster_host、master_port
> poffset
>```
### 7.5.2. 通知
- sentinel 在通知阶段要不断的去获取 master/slave 的信息,然后在各个sentinel 之间进行共享,具体的流程如下
### 7.5.3. 故障转移
- 当master宕机后sentinel是如何知晓并判断出master是真的宕机了呢?我们来看具体的操作流程
- 当sentinel认定master下线之后,此时需要决定更换master,那这件事由哪个sentinel来做呢?这时候sentinel之间要进行选举,如下图所示
- 在选举的时候每一个人手里都有一票,而每一个人的又都想当这个处理事故的人,那怎么办?
- 大家就开始抢,于是每个人都会发出一个指令,在内网里边告诉大家我要当选举人
- 比如说现在的 sentinel1 和 sentinel4 发出这个选举指令了,那么sentinel2 既能接到 sentinel1 的也能接到 sentinel4 的,接到了他们的申请以后呢,sentinel2 他就会把他的一票投给其中一方,投给谁呢?
- **谁先过来我投给谁**
- 假设 sentinel1 先过来,所以这个票就给到了sentinel1,那么给过去以后呢,现在 sentinel1 就拿到了一票,按照这样的一种形式,最终会有一个选举结果
- 对应的选举最终得票多的,那自然就成为了处理事故的人
- 需要注意在这个过程中**有可能会存在失败的现象**,就是一轮选举完没有选取,那就会接着进行第二轮第三轮直到完成选举。
- 接下来就是由选举胜出的sentinel去从slave中选一个新的master出来的工作
- **首先它有一个在服务器列表中挑选备选master的原则**
- 不在线的OUT
- 响应慢的OUT
- 与原master断开时间久的OUT
- 优先原则
- 优先级 : offset、runid
- 选出新的master之后,发送指令(sentinel )给其他的slave
- 向新的master发送slaveof no one
- 向其他slave发送slaveof 新masterIP端口
- **总结**:故障转移阶段
- 发现问题,主观下线与客观下线
- 竞选负责人
- 优选新master
- 新master上任,其他slave切换master,原master作为slave故障恢复后连接
# 8. Redis Cluster 集群
- 现状问题 : 业务发展过程中遇到的峰值瓶颈
- redis 提供的服务 OPS 可以达到 10 W/秒,当前业务 OPS 已经达到 10W/秒
- 内存单机容量达到 256G,当前业务需求内存容量 1T
- 使用集群的方式可以快速解决上述问题
## 8.1. 集群简介
- 集群就是使用网络将若干台计算机联通起来,并提供统一的管理方式,使其对外呈现单机的服务效果
- 集群的作用
- 分散单台服务器的访问压力,实现负载均衡
- 分散单台服务器的存储压力,实现可扩展性
- 降低单台服务器宕机带来的业务灾难
## 8.2. Cluster 集群结构设计
- **数据存储设计**
- 通过算法设计,计算出 key 应该保存的位置
- 将所有的存储空间计划切成 16384(2的14次) 份,每台主机保存一部分
- 注意: 每份代表的是一个存储空间,不是一个 key 的保存空间
- 将 key 按照计算出的结果放到对应的存储空间
- Redis 集群水平扩容
- 将 slot 分派一部分到新的节点上
- Redis集群查找数据
- 各个数据库相互通信,保存各个库中槽的编号数据
- 读写请求哪台Redis承接都可以,只要知道了 key,那存取数据是在哪一台服务器上那就是定死的
- 一次命中,直接返回
- 一次未命中,告知具体位置
## 8.3. Cluster 集群结构搭建
- 详见 [Redis 集群安装文档](https://www.ydlclass.com/doc21xnv/java/third/framework/4%E3%80%81redis/#_3%E3%80%81cluster%E9%9B%86%E7%BE%A4%E7%BB%93%E6%9E%84%E6%90%AD%E5%BB%BA)
- 首先要明确的几个要点:
- 配置服务器(3主3从)
- 建立通信(Meet)
- 分槽(Slot)
- 搭建主从(master-slave)
- 1. 创建6个redis单体服务 7001-7006。修改redis.conf (我们要搭建三主三从的集群架构)
>```
>port 7001
>bind 192.168.200.131
>protected-mode no
>daemonize yes
>pidfile /var/run/redis_7001.pid
>logfile "/export/server/redis7001/log/redis.log"
>dir /export/server/redis7001/data/
>appendonly yes
>cluster-enabled yes
>cluster-config-file nodes-7001.conf
>cluster-node-timeout 15000
>```
- 2. 让六台机器组成集群
- redis-cli --cluster create [master_host_port1,master_host_port2...] [slave_host_port1,slave_host_port2...] --cluster-replicas n
- 这个命令比较智能,会根据副本数自动分配 slave 个数
>```
>redis-cli --cluster create 192.168.200.131:7001 192.168.200.131:7002 192.168.200.131:7003 192.168.200.131:7004 192.168.200.131:7005 192.168.200.131:7006 --cluster-replicas 1
>```
## 8.4. java API 操作 Redis Cluster
### 8.4.1. 原生java代码操作
>```java
>package com.ydlclass.redis;
>
>import org.testng.annotations.AfterTest;
>import org.testng.annotations.BeforeTest;
>import org.testng.annotations.Test;
>import redis.clients.jedis.HostAndPort;
>import redis.clients.jedis.JedisCluster;
>import redis.clients.jedis.JedisPoolConfig;
>
>import java.io.IOException;
>import java.util.HashSet;
>import java.util.Set;
>
>/**
> * @Created by IT李老师
> * 公主号 “IT李哥交朋友”
> * 个人微 itlils
> */
>public class RedisClusterTest {
> JedisCluster jedisCluster;
>
> @BeforeTest
> public void beforeTest(){
> //创建jedis连接池
> JedisPoolConfig config=new JedisPoolConfig();
> //最大空闲连接
> config.setMaxIdle(10);
> //最小空闲连接
> config.setMinIdle(5);
> //最大空闲时间
> config.setMaxWaitMillis(3000);
> //最大连接数
> config.setMaxTotal(50);
>
> Set nodes=new HashSet<>();
> nodes.add(new HostAndPort("192.168.200.131", 7001));
> nodes.add(new HostAndPort("192.168.200.131", 7002));
> nodes.add(new HostAndPort("192.168.200.131", 7003));
> nodes.add(new HostAndPort("192.168.200.131", 7004));
> nodes.add(new HostAndPort("192.168.200.131", 7005));
> nodes.add(new HostAndPort("192.168.200.131", 7006));
>
> jedisCluster= new JedisCluster(nodes,config);
> }
>
> @Test
> public void addTest(){
> jedisCluster.set("c", "d");
> String str = jedisCluster.get("c");
> System.out.println(str);
> }
>
> @AfterTest
> public void afterTest(){
> try {
> jedisCluster.close();
> } catch (IOException e) {
> e.printStackTrace();
> }
> }
>
>}
>```
# 9. Redis 高频面试题
## 9.1. 缓存预热 (提前加载缓存数据)
### 9.1.1. 场景
- 场景: 宕机,服务器启动后迅速宕机
### 9.1.2. 问题排查
- 1. 请求数量较高,大量请求过滤之后都需要从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 Redis 的高强度操作从而导致问题
- 2. 主从之间数据吞吐量较大,数据同步操作频度较高
### 9.1.3. 解决方案
- **前置准备工作**
- 日常例行统计数据访问记录,统计访问频度较高的热点数据
- 利用 LRU 数据删除策略,构建数据留存队列
- 例如 storm 与 kafka 进行配合
- **准备工作**
- 将统计结果中的数据分类,根据数据级别,redis 优先加载级别较高的热点数据
- 利用分布式多服务器同时进行数据读取,提速数据加载过程
- 热点数据主从同时预热
- **实施**
- 使用脚本程序固定触发数据预热过程
- 如果条件允许,使用了 CDN (内容分发网络),效果会更好
- 总的来说,**缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统**。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
## 9.2. 缓存雪崩 (大量 key 集中过期)
### 9.2.1. 场景
- 场景: 数据库服务器崩溃,一连串的场景会随之而来
- 1. 系统平稳运行中,忽然数据库连接量激增
- 2. 应用服务器无法及时处理请求
- 3. 大量 408、500 错误页面出现
- 4. 客户反复刷新页面获取数据
- 5. 数据库崩溃
- 6. 应用服务器崩溃
- 7. 重启应用服务器失效
- 8. Redis 服务器崩溃
- 9. Redis 集群崩溃
- 10. 重启数据库后再次被瞬间流量放倒
### 9.2.2. 问题排查
- 1. 在一个较短的时间内,缓存中较多的 key 集中过期
- 2. 此周期内请求访问过期的数据,redis 未命中,redis 向数据库获取数据
- 3. 数据库同时接收到大量的请求无法及时处理
- 4. Redis大量请求被积压,开始出现超时现象
- 5. 数据库流量激增,数据库崩溃
- 6. 重启后仍然面对缓存中无数据可用
- 7. Redis 服务器资源被严重占用,Redis 服务器崩溃
- 8. Redis 集群呈现崩塌,集群瓦解
- 9. 应用服务器无法及时得到数据响应请求,来自客户端的请求数量越来越多,应用服务器崩溃
- 10. 应用服务器、redis、数据库全部重启,效果不理想
- 总而言之就是两点: **短时间范围内,大量 key 集中过期**
### 9.2.3. 解决方案
- **思路**
- 更多的页面静态化处理
- 构建多级缓存架构
- Nginx 缓存 + Redis 缓存 + ehcache 缓存
- 检测 Mysql 严重耗时业务进行优化
- 对数据库的瓶颈排查: 例如超时查询、好事较高事务等
- 灾难预警机制
- 监控 Redis 服务器性能指标: CPU 占用、CPU 使用率、内存容量、查询平均响应时间、线程数
- 限流、降级
- 短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问。
- **落地实践**
- LRU与LFU切换
- 数据有效期策略调整
- 根据业务数据有效期进行分类错峰,A类90分钟,B类80分钟,C类70分钟
- 过期时间使用固定时间+随机值的形式,稀释集中到期的key的数量
- 超热点数据使用永久 key
- 定期维护 (自动 + 人工)
- 对即将过期数据做访问量分析,确认是否掩饰,配合访问量统计,做热点数据的延时
- 加锁 : 慎用!
- 总的来说
- **缓存雪崩就是瞬间过期数据量太大**,导致对数据库服务器造成压力。
- 如能够有效避免过期时间集中,可以有效解决雪崩现象的 出现(约40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。
## 9.3. 缓存击穿 (热点key过期,访问量巨大)
### 9.3.1. 场景
- 场景: 还是数据库服务器崩溃,但是和之前场景略有不同 (Redis 运行正常,数据库崩溃)
- 系统平稳运行过程中
- 数据库连接量瞬间激增
- Redis 服务器内存平稳,无波动
- Redis 服务器 CPU 正常
- 数据库崩溃
### 9.3.2. 问题排查
- 1. Redis 中某个 key 过期,该 key 访问量巨大
- 2. 多个数据请求从服务器直接压到 Redis 后,均未命中
- 3. Redis 在短时间内发起了大量对数据库中同一数据的访问
- 总而言之就两点 : **单个 key 高热点数据,key 过期**
### 9.3.3. 解决方案
- 1. 预先设定
- 以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息key的过期时长
- 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势
- 2. 现场调整
- 监控访问量,对自然流量激增的数据演唱过期时间或者设置未永久性 key
- 3. 后台刷新数据
- 启动定时任务,高峰期来临之前,刷新数据有效性,确保不丢失
- 4. 二级缓存
- 设置不同的失效时间,保障不会被同时淘汰就行
- 5. 加锁
- 分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重使用
- 总的来说
- **缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力**。
- 应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可。
## 9.4. 缓存穿透 (查了不存在的数据,key大面积未命中)
### 9.4.1. 场景
- 场景: 数据库服务器崩溃
- 系统平稳运行过程中
- 应用服务器流量随时间增量较大
- Redis 服务器命中率随时间逐步降低
- Redis 内存平稳,内存无压力
- Redis 服务器 CPU 占用激增
- 数据库服务器压力激增
- 数据库崩溃
### 9.4.2. 问题排查
- Redis 中 key 大面积未命中
- 出现非正常 URL 访问
### 9.4.3. 问题分析
- 获取的数据在数据库中也不存在,数据库查询未得到对应数据
- Redis 获取到 null 数据未进行持久化,直接返回
- 下次此类数据,到达重复上述过程
- 出现黑客攻击服务器
### 9.4.4. 解决方案
- 1. 缓存 null
- 对查询结果为 null 的数据进行缓存 (长期使用,定期清理),设定短时限,例如 30-60 秒,最高 5 分钟
- 2. 白名单策略
- 提前预热各种分类数据 di 对应的 bitmaps, id 作为 bitmaps 的 offset,相当于设置了数据白名单
- 加载数据正常数据时放行,加载异常数据时直接拦截 (效率偏低)
- 使用 布隆过滤器
- 3. 实施监控
- 实时监控 redis 命中率 (业务正常范围时,通常会有一个波动值) 与 null 值的占比
- 非活动时段波动: 通常检测 3-5 倍,超过 5 倍纳入重点排查对象
- 活动时段波动: 通常检测 10-50 倍
- 根据倍数不同,启动不同的排查流程,然后使用黑名单进行防控 (运营)
- 4. key 加密
- 问题出现后,临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验
- 例如,每天随机分配 60 个加密单,提奥选 2-3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问
- 总体来说
- **缓存击穿是指访问了不存在的数据,跳过了合法数据的 redis 数据缓存阶段,每次访问数据库,导致对数据库造成压力**,通常此类数据的出现量是一个较低的值,当出现此类情况,以毒攻毒,并及时报警。应对策略应该在临时防范方面多做文章
- 无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除
## 9.5. Redis 命名规范
- 1. 使用统一的命名规范
- 一般使用业务名 (或数据库名) 为前缀,用 ":" 分隔
- 例如,业务名:表名:id
- 举例, shop:usr:msg_code (电商:用户:验证码)
- 2. 控制 key 名称的长度,不要使用过长的 key
- 在保证语义清晰的情况西,尽量减少 key 的长度。有些常用单词可使用缩写。例如,user 可以使用 u,message 可以使用 msg
- 3. 名称中不要包含特殊字符,例如空格、单双引号以及其他转义字符
## 9.6. 性能指标监控
- Redis 中的监控指标如下
- 1. **性能指标:Performance**
- 响应请求的平均时间: latency
- 平均每秒处理请求总数: instantaneous_ops_per_sec
- 缓存查询命中率 (通过查询总次数与查询得到非nil数据总次数计算而来): hit_rate(calculated)
- 2. **内存指标: Memory**
- 当前内存使用量: used_memory
- 内存碎片率 (关系到是否进行碎片整理): mem_fragmentation_ratio
- 为避免内存溢出删除的 key 的总数量: evicted_keys
- 基于阻塞操作(BLPOP等)影响的客户端数量: blocked_clients
- 3. **基本活动指标 : Basic_acticity**
- 当前客户端连接总数: connected_clients
- 当前连接 slave 总数: connected_slaves
- 最后一次主从信息交换距现在的秒: master_last_io_seconds_ago
- key 的总数: keyspace
- 3. **持久性指标: Persistence**
- 当前服务器最后一次RDB持久化的时间: rdb_last_save_time
- 当前服务器最后一次RDB持久化后数据变化总量: rdb_changes_since_last_save
- 5. **错误指标: Error**
- 被拒绝连接的客户端总数 (基于达到最大连接值的因素): rejected_connections
- key 未命中的总次数: keyspace_misses
- 主从断开的秒数: master_link_down_since_seconds