1 Linux 环境安装 Redis
1.1 环境准备
- CentOS 7.x 。
- redis-5.0.10.tar.gz 。
1.2 Linux 环境安装 Redis
- 切换到 /opt 目录下:
cd /opt
- 下载 Redis 的安装包:
wget https://download.redis.io/releases/redis-5.0.10.tar.gz
- 解压 Redis :
tar -zxvf redis-5.0.10.tar.gz
- 进入 Redis 的解压目录:
cd redis-5.0.10
- 安装 C 语言编译环境(需要联网):
yum -y install gcc-c++
- 可选操作(安装最新的 gcc 编译器):
yum -y install centos-release-scl scl-utils-build
yum -y install devtoolset-8-toolchain
scl enable devtoolset-8 bash
- 修改安装的目录路径:
vim src/Makefile
# 就 Redis 自身而言是不需要修改的,这里修改的目的是让 Redis 的运行程序不要和其他文件混杂在一起。
PREFIX?=/usr/local/redis
- 编译安装:
make && make install
1.3 启动 Redis 服务器
1.3.1 默认启动
- 启动:
cd /usr/local/redis/bin
./redis-server
默认 Redis 启动是以前台进程的方式启动,实际开发中,需要以后台守护进程的方式启动。
1.3.2 定制配置启动
- 从解压目录中复制配置文件:
cp /opt/redis-5.0.10/redis.conf /usr/local/redis/
- 修改配置项:
vim redis.conf
配置项名称 | 作用 | 取值 |
---|---|---|
daemonize | 控制是否以守护进程形式运行 Redis 服务器 | yes |
logfile | 指定日志文件位置 | “/var/logs/redis.log” |
dir | 设定当前服务文件保存位置,包括日志文件、持久化文件等 | /usr/local/redis |
注意:/var/logs 目录需要我们提前创建好。
- 通过配置文件启动:
cd /usr/local/redis/bin
./redis-server /usr/local/redis/redis.conf
2 Redis 持久化
2.1 持久化简介
2.1.1 什么是持久化?
- 利用永久性的存储介质(如:硬盘等)将数据进行保存,在特定的时间,将保存的数据进行恢复的工作机制称为持久化。
2.1.2 为什么要进行持久化?
- 防止数据的意外丢失,确保数据安全。
2.1.3 持久化过程中保存什么?
- 将当前数据状态进行保存(类似于快照形式),存储数据结果,存储格式简单,关注点在于数据。
- 将数据的操作过程进行保存(类似于日志形式),存储操作过程,存储格式复杂,关注点在于数据操作。
2.2 RDB
2.2.1 RDB 启动方式 — save 指令
- 命令:
save
作用:手动执行一次保存操作。
示例:
keys *
set name 123
save
2.2.2 RDB 启动方式 — save 指令相关配置
- 技巧:将 redis.conf 文件的注释和空格去掉
cat /opt/redis-5.0.10/redis.conf | grep -v "#" | grep -v "^$" > redis.conf.bak
- save 指令相关配置:
- ①
dbfilename dump.rdb
:- 说明:设置本地数据库文件名,默认值为
dump.rdb
。 - 经验:通常设置为
dump-端口号.rdb
。
- 说明:设置本地数据库文件名,默认值为
- ②
dir ./
:- 说明:设置存储
.rdb
文件的路径。 - 经验:通常设置成存储空间较大的目录中,目录名称
data
。
- 说明:设置存储
- ③
rdbcompression yes
:- 说明:设置存储到本地数据库是否压缩数据,默认为
yes
,采用 LZF 压缩。 - 经验:通常默认为开启状态,如果设置为 no ,可以节省 CPU 运行时间,但是会使得压缩文件变得很大。
- 说明:设置存储到本地数据库是否压缩数据,默认为
- ④
rdbchecksum yes
:- 说明:设置是否进行 RDB 文件格式校验,该校验过程在写文件和读文件过程均进行。
- 经验:通常默认为开启状态,如果设置为 no ,可以节约读写性过程约 10% 时间消耗,但是存在一定的数据损坏风险。
2.2.3 RDB 启动方式 — save 指令工作原理
注意:save 指令的执行会阻塞当前 Redis 服务器,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,
线上环境不建议使用
。
2.2.4 RDB 启动方式 — bgsave 指令
- 问:数据量过大,单线程执行方式造成效率过低如何处理?
答:后台执行。
命令:
bgsave
- 作用:手动启动后台保存操作,但是不是立即执行。
2.2.5 RDB 启动方式 — bgsave 指令工作原理
注意:bgsave 指令是针对 save 阻塞问题做的优化。Redis 内部所有涉及到 RDB 操作都可以采用 bgsave 指令的方式,
save 指令可以放弃使用
。
2.2.6 RDB 启动方式 — bgsave 指令相关配置
- ①
dbfilename dump.rdb
:略(和 save 指令相关配置相同)。 - ②
dir ./
:略。 - ③
rdbcompression yes
:略。 - ④
rdbchecksum yes
:略。 - ⑤
stop-writes-on-bgsave-error yes
:- 说明:后台存储过程中如果出现错误现象,是否停止保存操作。
- 经验:通常默认为开启状态。
2.2.7 RDB 启动方式 — save 配置
- 问:反复执行保存指令,忘记了怎么办?不知道数据产生了多少变化,何时保存?
答:自动执行。
配置(redis.conf):
save second changes
- 作用:满足限定时间内 key 的变化数量达到指定数量就进行持久化操作。
参数:
- second :监控时间范围。
- changes:监控 key 的变化量。
示例:
save 900 1
save 300 10
save 60 10000
2.2.8 RDB 启动方式 — save 配置工作原理
注意:
- save 配置要根据实际业务情况设置,频度过高或过低都会出现性能问题,结果可能是灾难性的。
- save 配置中对于 second 和 changes 设置通常具有互补对应关系,尽量不要设置成包含关系。
- save 配置启动后执行的是 bgsave 操作。
2.2.9 总结
- RDB 三种启动方式对比: | 方式 | save 指令 | bgsave 指令 | save 配置 | | —- | —- | —- | —- | | 读写 | 同步 | 异步 | 异步 | | 阻塞客户端指令 | 是 | 否 | 否 | | 额外内存消耗 | 否 | 是 | 是 | | 启动新进程 | 否 | 是 | 是 |
RDB 特殊启动形式:
- ① 主从复制:在主从复制中详细讲解。
② 服务器运行过程中重启。
debug reload
③ 关闭服务器时指定保存数据:
shutdown save
RDB 优点:
- ① RDB 是一个紧凑压缩的二进制文件,存储效率高。
- ② RDB 内存存储的是 Redis 在某个
时间点
的数据快照,非常适合用于数据备份,全量复制等场景。 - ③ RDB 恢复数据的速度要比 AOF 快很多。
- ④ 应用:服务器中每 x 小时执行 bgsave 备份,并将 RDB 文件拷贝到远程机器中,用于灾难恢复。
- RDB 缺点:
- ① RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据。
- ② bgsave 指令每次运行要执行 fork 操作创建子进程,要牺牲掉一些性能。
- ③ Redis 的众多版本中未进行 RDB 文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象。
2.3 AOF
2.3.1 RDB 存储的弊端
- ① 存储数据量较大,效率较低:基于快照思想,每次读写都是全部数据,当数据量巨大的时候,效率非常低。
- ② 大数据量下的 IO 性能较低。
- ③ 基于 fork 创建子进程,内存产生额外消耗。
- ④ 如果不是实时做快照,那么宕机会带来数据丢失的风险。
2.3.2 解决思路
- ① 不写全数据,仅记录部分数据。
- ② 改记录数据为记录操作过程。
- ③ 对所有操作均进行记录,排除丢失数据的风险。
2.3.3 AOF 简介
- AOF(Append Only File)持久化:以独立日志的方式记录每次命令,重启时再重新执行 AOF 文件中命令达到恢复数据的目的,和 RDB 相比可以描述为
改记录数据为记录数据产生的过程
。 - AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
2.3.4 AOF 写数据过程
2.3.5 AOF 写数据的三种策略(appendfsync)
- ① always(每次):每次写入操作均同步到 AOF 文件中,
数据零误差,性能较低
(大数据量下时,系统的资源被 IO 大量占用,那么业务系统的性能将会有所影响),不建议使用。 - ② everysec(每秒):
- 每秒将缓冲区中的指令同步到 AOF 文件中,
数据准确性较高,性能较高
。 - 在系统突然宕机的情况下丢失 1 秒内的数据。
- 建议使用,也是默认配置。
- 每秒将缓冲区中的指令同步到 AOF 文件中,
- ③ no(系统控制):由操作系统控制每次同步到 AOF 文件的周期,
整体过程不可控
,不建议使用。
2.3.6 AOF 功能开启
- ①
appendonly yes|no
:是否开启 AOF 持久化功能,默认为不开启状态。 - ②
appendfsync always|everysec|no
:AOF 写数据策略,默认为 everysec 。 - ③
dir ./
:AOF 持久化文件保存路径,与 RDB 持久化文件保持一致即可。 - ④
appendfilename "appendonly.aof"
:AOF 持久化文件名,默认文件名为 appendonly.aof ,建议配置为 appendonly-端口号.aof 。
2.3.7 AOF 重写
- 如果连续执行如下指令该如何处理?
- AOF 重写:随着命令不断写入 AOF,文件会变得越来越大,为了解决这个问题,Redis 引入了 AOF 重写机制来压缩文件体积。AOF 文件重写是将 Redis 进程内的数据转换为写命令同步到新 AOF 文件的过程。简单来说,就是将对统一数据的若干条命令结果转换成最终结果数据对应的指令进行记录。
- AOF 重写的作用:
- ① 降低磁盘占用量,提高磁盘利用率。
- ② 提高持久化效率,降低持久化写时间,提高 IO 性能。
- ③ 降低数据恢复用时,提高数据恢复效率。
- AOF 重写的规则:
- ① 进程内已超时的数据不再写入文件。
- ② 忽略无效指令,重写时使用进程内的数据直接生成,这样新的 AOF 文件只保存最终数据的写入命令。
- 如:del key1、hdel key2、srem key4、set key4 111、set key4 222等。
- ③ 对同一数据的多条命令合并为一条命令。
- 如:lpush list1 a、lpush list1 b、lpush list1 c 可以转为为 lpush list1 a b c。
- 为了防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等类型,每条指令最多写入 64 个元素。
2.3.8 AOF 重写方式
- 手动重写:
bgrewriteaof
- 自动重写:
auto-aof-rewrite-min-size size
auto-aof-rewrite-percentage percentage
- 自动重写触发比对参数(运行指令 info persistence 获取具体信息):
aof_current_size
aof_base_size
- 自动触发条件:
2.3.9 AOF 工作流程
2.3.10 AOF 重写流程
2.4 RDB 和 AOF 的区别
2.4.1 RDB VS AOF
持久化方式 | RDB | AOF |
---|---|---|
占用存储空间 | 小(数据级:压缩) | 大(指令级:重写) |
存储速度 | 慢 | 快 |
恢复速度 | 快 | 慢 |
数据安全性 | 会丢失数据 | 依据策略决定 |
资源消耗 | 高/重量级 | 低/轻量级 |
启动优先级 | 低 | 高 |
2.4.2 RDB 和 AOF 选择之惑
- 对数据非常敏感,建议使用默认的 AOF 持久化方案。
- AOF 持久化策略使用 everysec,每秒钟 fsync 一次。该策略 Redis 依然可以保持很好的处理性能,当出现问题的时候,最多丢失 0 ~ 1 秒内的数据。
- 注意:由于 AOF 文件存储体积较大,且恢复速度较慢。
- 数据呈现阶段有效性,建议使用 RDB 持久化方案。
- 数据可以良好的做到阶段内无丢失(该阶段是开发人员或运维人员手动维护的),且恢复速度较快,阶段性数据恢复通常采用 RDB 方案。
- 注意:利用 RDB 实现紧凑的数据持久化会使 Redis 性能下降的非常厉害(Redis 中的数据越多,频繁的进行 RDB 持久化,将会使得性能下降的比较厉害)。
- 总结:
- RDB 和 AOF 的选择实际上是做一种权衡,各有利弊。
- 如果不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF 。
- 如果能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB 。
- 灾难恢复选用 RDB 。
- 双保险策略,同时开启 RDB 和 AOF ,重启后,Redis 优先使用 AOF 来恢复数据,降低数据丢失的量。
2.5 持久化的应用场景
- ①
Redis 用于控制数据库表主键 id,为数据库表主键提供生成策略,保障数据库表的主键唯一性(不建议持久化,临时存储即可,因为如果 Redis 中的数据保存的主键 id 是 18,但是持久化到文件中却是 17 ,那么 Redis 宕机重启后,会恢复为 17 ,这样数据库表中就会造成主键重复的现象)。 - ②
Redis 应用于各种结构型和非结构型高热度数据访问加速(不建议持久化)。 - ③
Redis 应用于购物车数据存储设计(不建议持久化)。 - ④ Redis 应用于抢购、限购类、限量发放优惠券、激活码等业务的数据存储设计。
- ⑤ Redis 应用于具有操作先后顺序的数据控制。
- ⑥ Redis 应用于最新消息展示。
- ⑦
Redis 应用于同类信息的关联搜索、二度关联搜索、深度关联搜索。 - ⑧ Redis 应用于基于黑白名单设定的服务控制。
- ⑨ Redis 应用于计数器组合排序功能对应的排名。
- ⑩
Redis 应用于即时任务/消息队列执行管理(不建议,可以使用直接 MQ )。 - ⑪
Redis 应用于按次结算的服务控制。
3 SpringBoot 整合 Redis
3.1 准备工作
- IDEA 2021+。
- JDK 11+。
- Maven 3.8。
- SpringBoot:2.6.1。
- Redis 5.0.10。
- pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>redis</artifactId>
<version>1.0</version>
<name>redis</name>
<description>redis</description>
<properties>
<java.version>11</java.version>
<skipTests>true</skipTests>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.1</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>aliyunmaven</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring</id>
<url>https://maven.aliyun.com/repository/spring</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
3.2 配置文件
- application.yml
server:
port: 8081
spring:
redis: # redis
host: 127.0.0.1 # 主机 IP 地址
port: 6379 # 端口号
database: 0 # 数据库索引,默认为 0
timeout: 1800000 # 连接超时时间(毫秒),默认为 0 ,表示永不超时
lettuce:
pool:
max-active: 20 # 连接池的最大连接数(负数表示没有限制)
max-wait: -1 # 连接池的最大阻塞等待时间(负数表示没有限制)
max-idle: 5 # 连接池的最大空闲连接
min-idle: 0 # 连接池的最小空闲连接
client-type: lettuce
3.3 配置类
- RedisConfig.java
package com.github.fairy.era.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurationSelector;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* @author 许大仙
* @version 1.0
* @since 2021-12-13 11:02
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurationSelector {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
// key序列化方式
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
// value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
3.4 启动类
- RedisApplication.java
package com.github.fairy.era.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author xuwei
*/
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
3.5 Handler
- RedisHandler.java
package com.github.fairy.era.redis.handler;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 许大仙
* @version 1.0
* @since 2021-12-13 11:08
*/
@RestController
@RequestMapping("/redis")
@RequiredArgsConstructor
public class RedisHandler {
private final RedisTemplate redisTemplate;
@GetMapping("/save")
public void save() {
// 设置值
redisTemplate.opsForValue().set("name", "redis");
// 获取值
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
}
4 Redis 事务
4.1 事务简介
- Redis 事务就是一个命令执行的队列,将一系列预定义的命令包装成一个整体(一个队列)。当执行的时候,一次性按照添加的顺序依次执行,中间不会被打断或者干扰。
- 简而言之,Redis 事务就是一个队列中,一次性、顺序性、排他性的执行一系列命令。
4.2 事务的基本操作
- 开启事务:设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中
multi
- 执行事务:设置事务的结束位置,同时结束事务。和 multi 成对出现,成对使用
exec
注意:加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行 exec 命令才开始执行。
- 取消事务:终止当前事务的定义,发生在 multi 之后, exec 之前
discard
4.3 事务的工作流程
4.4 事务的注意事项
4.4.1 定义事务的过程中,命令格式输入错误怎么办?
- 语法错误:指的是命令书写错误。
处理结果:如果定义的事务中所包含的命令存在语法错误,整体事务所有命令均不执行,包括那些语法正确的命令。
示例:
multi
set name 123
get name
est name 231
exec
4.4.2 定义事务的过程中,命令执行出现错误怎么办?
- 运行错误:指的是命令格式正确,但是无法正确的执行。如:对 list 进行 incr 操作。
- 处理结果:能够正确运行的命令会执行,运行错误的命令不会被执行。
注意:已经执行完毕的命令对应的数据不会自动回滚,需要程序员自己在代码中实现回滚。
- 示例:
multi
set name 123
get name
lpush name a b c
get name
exec
keys *
4.4.3 手动进行事务回滚(不推荐)
- 记录操作过程中被影响的数据之前的状态。
- 单数据:string。
- 多数据:hash、list、set、zset。
- 设置指令恢复所有的被修改的项:
- 单数据:直接 set (注意周边属性,例如:时效等)。
- 多数据:修改对应值或者整体复制。
4.5 锁
4.5.1 锁
- 业务场景:天猫双 11 热卖过程中,对已经售罄的货物追加补货,4 个业务员都有权限进行补货。补货的操作可能是一系列的操作,涉及到多个连续操作,如何保证不会重复操作?
- 业务分析:
- 多个客户端可能同时操作同一组数据,并且该数据一旦被操作修改后,将不适用于继续操作。
- 在操作之前锁定要操作的数据,一旦发生变化,终止当前操作。
解决方案:
对 key 添加监视锁,在执行 exec 前如果 key 发生了变化,终止事务执行:
watch key1 [key2]...
取消对所有 key 的监视:
unwatch
注意:Redis 中的这种锁是一种乐观锁的体现。
- 示例:
# 客户端1
set num 123
# 客户端1
get num
# 客户端1
watch num
# 客户端1
multi
incr num
# 客户端2
incr num
# 客户端1
exec
4.5.2 分布式锁
- 业务场景:天猫双 11 热卖过程中,对已经售罄的货物追加补货,且补货完成。客户购买热情高涨,3 秒内将所有商品购买完毕。本次补货已经将库存全部清空,如何避免最后一件商品不被多人同时购买(超卖问题)?
- 业务分析:
- 使用 watch 监控一个 key 有没有改变不能解决问题(商品的数量是不停的变化,难道一个人购买了,其他人的订单取消掉?),此处要监控的是具体数据。
- 虽然 Redis 是单线程的,但是多个客户端对同一数据同时进行操作时,如何避免不被同时修改?
解决方案:
使用 setnx 设置一个公共锁:
setnx lock-key value
利用 setnx 命令的返回值特征,有值则返回设置失败,没有值则返回设置成功。
- 对于返回设置成功的,拥有控制权,进行下一步的具体业务操作。
- 对于返回设置失败的,不具有控制权,排队或者等待。
- 操作完毕后通过 del 操作释放锁。
注意:上述解决方案是一种设计概念,依赖规范保障,具有风险性。
4.5.3 死锁
- 业务场景:依赖分布式锁的机制,某个用户操作时对应客户端宕机了,且此时已经获取到了锁,如何解决?
- 业务分析:
- 由于锁操作是由用户控制加锁解锁,必然存在加锁后没有解锁的风险。
- 需要解锁的操作不能依赖用户控制,系统级别要给出对应的保底处理方案。
解决方案:
- 使用 expire 为锁 key 添加时间限定,到时不释放,就放弃锁:
expire lock-key second
pexpire lock-key milliseconds
- 使用 expire 为锁 key 添加时间限定,到时不释放,就放弃锁:
由于操作通常都是毫秒级或微妙级的,因此锁设定额时间不宜过长,具体时间需要业务测试后确认。
- 例如:持有锁的操作最长执行时间为 127 ms,最短执行时间为 7 ms。
- 测试百万次最长执行时间对应命令的最长耗时,测试百万次网络延迟平均耗时。
- 锁时间设定推荐:
最长耗时 * 120 % + 平均网络延迟 * 110 %
。 - 如果业务最长耗时 << 网络平均延迟,通常为 2 个数量级,取其中单个耗时较长即可。
5 秒杀案例
5.1 基本实现
5.1.1 需求
5.1.2 技术栈
- SpringBoot。
- vue。
- axios。
- thymeleaf。
- Redis。
5.1.3 代码实现
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Mac Book Pro 1.0 元秒杀</h1>
<form id="form" method="post">
<input :value="productId" type="hidden">
<button @click="secondKill(productId)" type="button">点我秒杀</button>
<div>{{secondKillReturnMsg}}</div>
<br><br>
<button @click="addInventory(productId)" type="button">增加库存</button>
<div>{{addInventoryReturnMsg}}</div>
</form>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/qs/6.9.4/qs.min.js"></script>
<script>
new Vue({
el: '#form',
data: {
productId: '0101',
secondKillReturnMsg: '',
addInventoryReturnMsg: ''
},
methods: {
secondKill(productId) {
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
axios.post("/secondKill", window.Qs.stringify({productId})).then(res => {
console.log(res.data);
this.secondKillReturnMsg = res.data
})
},
addInventory(productId) {
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
axios.post("/addInventory", window.Qs.stringify({productId})).then(res => {
console.log(res.data);
this.addInventoryReturnMsg = res.data
})
}
}
})
</script>
</body>
</html>
- RedisConfig.java
package com.github.fairy.era.redis.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurationSelector;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* @author 许大仙
* @version 1.0
* @since 2021-12-13 11:02
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurationSelector {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
// key序列化方式
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
// value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
- ExceptionAdvice.java
package com.github.fairy.era.redis.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author 许大仙
* @version 1.0
* @since 2021-12-13 16:10
*/
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(RuntimeException ex) {
log.info("ExceptionAdvice.handleRuntimeException ==> {}", ex.getMessage());
return ex.getMessage();
}
}
- SecondKillService.java
package com.github.fairy.era.redis.service;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
/**
* @author 许大仙
* @version 1.0
* @since 2021-12-13 15:15
*/
@Service
@RequiredArgsConstructor
public class SecondKillService {
private final RedisTemplate redisTemplate;
/**
* 秒杀逻辑
*
* @param productId 产品的id
* @param userId 用户的id
*/
public boolean secondKill(String productId, String userId) {
// ① productId 和 userid 的非空校验
if (StrUtil.isBlank(productId) || StrUtil.isBlank(userId)) {
return false;
}
// ② 定义 Redis 中的 key:库存的 key 和秒杀成功用户的 key
String inventorykey = "sk:" + productId + ":qt";
String userkey = "sk:" + productId + ":user";
// ③ 获取 Redis 中的库存,如果库存为 null,那么秒杀还没开始
String inventory = String.valueOf(redisTemplate.opsForValue().get(inventorykey));
if (StrUtil.isBlankOrUndefined(inventory)) {
throw new RuntimeException("秒杀还没开始,请等待。。。");
}
// ④ 判断用户是否重复秒杀
if (redisTemplate.opsForSet().isMember(userkey, userId)) {
throw new RuntimeException(userId + "已经秒杀成功了,无需重复秒杀!!");
}
// ⑤ 如果库存的数量小于 1 ,秒杀结束
if (Long.parseLong(inventory) < 1) {
throw new RuntimeException("库存的数量小于 1 ,秒杀结束");
}
// ⑦ 秒杀
// 库存 -1
redisTemplate.opsForValue().decrement(inventorykey);
// 秒杀成功用户增加到秒杀成功清单中
redisTemplate.opsForSet().add(userkey, userId);
return true;
}
/**
* 增加库存
*/
public void addInventory(String productId) {
String inventorykey = "sk:" + productId + ":qt";
redisTemplate.opsForValue().set(inventorykey, 10);
}
}
- SecondKillHandler.java
package com.github.fairy.era.redis.handler;
import com.github.fairy.era.redis.service.SecondKillService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
/**
* @author 许大仙
* @version 1.0
* @since 2021-12-13 14:55
*/
@RestController
@RequiredArgsConstructor
@Slf4j
public class SecondKillHandler {
private final SecondKillService secondKillService;
/**
* 秒杀
*
* @param productId
* @return
*/
@PostMapping(value = "/secondKill")
public String secondKill(String productId) {
log.info("productId是:{}", productId);
String userId = String.valueOf(new Random().nextInt(50000));
log.info("userId是:{}", userId);
boolean flag = secondKillService.secondKill(productId, userId);
if (flag) {
return "秒杀成功";
}
return "秒杀失败";
}
/**
* 增加库存
*
* @return
*/
@PostMapping(value = "/addInventory")
public String addInventory(String productId) {
secondKillService.addInventory(productId);
return "增加库存成功";
}
}
- 启动类:
package com.github.fairy.era.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author xuwei
*/
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
5.2 Redis 事务 — 秒杀并发模拟
- CentOS 7 执行如下命令:
yum -y install httpd-tools
- 通过命令来发送请求:
# -n 表示请求的总数
# -c 表示并发数
# -T 如果是 PUT 或 POST 请求,需要此参数,设置 Content-Type
# -P 表示 POST请求 携带的参数所在的文件
ab -n 2000 -c 200 -k -p ~/postfile -T 'application/x-www-form-urlencoded' http://localhost:8081/secondKill
- 将 springboot 项目打包部署到 Linux服务器(部署了 Redis )中,执行如下命令:
vim ~/postfile
productId=0101&
ab -n 1000 -c 100 -k -p ~/postfile -T 'application/x-www-form-urlencoded' http://localhost:8081/secondKill
- 出现了超卖问题:
5.3 使用 Redis 的乐观锁机制来解决超卖问题
- SecondKillService.java
package com.github.fairy.era.redis.service;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* @author 许大仙
* @version 1.0
* @since 2021-12-13 15:15
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SecondKillService {
private final RedisTemplate redisTemplate;
/**
* 秒杀逻辑
*
* @param productId 产品的id
* @param userId 用户的id
*/
public boolean secondKill(String productId, String userId) {
// ① productId 和 userid 的非空校验
if (StrUtil.isBlank(productId) || StrUtil.isBlank(userId)) {
return false;
}
// ② 定义 Redis 中的 key:库存的 key 和秒杀成功用户的 key
String inventorykey = "sk:" + productId + ":qt";
String userkey = "sk:" + productId + ":user";
List<Object> execute = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.watch(inventorykey);
// ③ 获取 Redis 中的库存,如果库存为 null,那么秒杀还没开始
String inventory = String.valueOf(operations.opsForValue().get(inventorykey));
if (StrUtil.isBlankOrUndefined(inventory)) {
throw new RuntimeException("秒杀还没开始,请等待。。。");
}
// ④ 判断用户是否重复秒杀
if (operations.opsForSet().isMember(userkey, userId)) {
throw new RuntimeException(userId + "已经秒杀成功了,无需重复秒杀!!");
}
// ⑤ 如果库存的数量小于 1 ,秒杀结束
if (Long.parseLong(inventory) < 1) {
throw new RuntimeException("库存的数量小于 1 ,秒杀结束");
}
// ⑦ 秒杀
operations.multi();
// 库存 -1
operations.opsForValue().decrement(inventorykey);
// 秒杀成功用户增加到秒杀成功清单中
operations.opsForSet().add(userkey, userId);
return operations.exec();
}
});
if (CollectionUtils.isEmpty(execute)) {
log.info("秒杀失败");
} else {
log.info("秒杀成功");
}
return true;
}
/**
* 增加库存
*/
public void addInventory(String productId) {
String inventorykey = "sk:" + productId + ":qt";
redisTemplate.opsForValue().set(inventorykey, 10);
}
}
5.4 库存的遗留问题
5.4.1 问题复现
- 将库存设置大点:
set sk:0101:qt 500
- 请求和并发数也设置大点:
ab -n 2000 -c 300 -k -p ~/postfile -T 'application/x-www-form-urlencoded' http://localhost:8081/secondKill
- 会发现如下问题,已经秒杀完,但是还有库存。
- 原因:
乐观锁导致很多请求都失败,先点的没有秒杀到,后点的可能秒杀到了
。
5.4.2 Lua
- Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
- 很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
- 这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。
5.4.3 Lua 脚本在 Redis 中的优势
- 将复杂的或者多步的 Redis 操作,写成一个脚本,一次提交给 Redis 执行,减少反复连接 Redis 的次数,以便提升性能。
- lua 脚本类似于 Redis 中的事务,有一定的原子性,不会被其他命令插队,可以完车一些 Redis 的事务操作。
注意:Redis 中的 lua 脚本功能,只有在 2.6 以上的版本才可以使用。
- 可以利用 lua 脚本淘汰用户,解决超卖问题。
- 通过 lua 脚本解决争抢问题,其实就是 Redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
6 删除策略
6.1 过期数据
6.1.1 Redis 中的数据特征
- Redis 是一种内存级别的数据库,所有的数据均存放在内存中,内存中的数据可以通过 ttl 指令获取其状态。
- xx:具有时效性的数据。
- -1:永久有效的数据。
- -2:
已经过期的数据
或被删除的数据或者未定义的数据。
删除策略就是针对已经过期的数据的处理策略
,已经过期的数据真的立即删除吗?其实未必,Redis 中有很多种删除策略,是分情况的,在不同的场景下使用不同的删除方式会有不同的效果。
6.1.2 时效性数据的存储结构
- 在 Redis 中,如何给数据设置它的失效周期?数据的时效在 Redis 中是如何存储的?
- 过期数据是一块独立的存储空间,是 hash 结构,field 是内存地址,value 是过期时间,保存了所有 key 的过期描述,在最终进行过期处理的时候,对该空间的数据进行检测,当时间到期之后,通过 field 找到对应的该内存地址的数据,然后进行相关的操作。
6.2 数据删除策略
6.2.1 数据删除策略的目标
- 在内存占用和 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露。
6.2.2 数据删除策略的分类
- ① 定时删除。
- ② 惰性删除。
- ③ 定期删除。
6.2.3 定时删除
- 创建一个定时器,当 key 设置有过期时间,且过期时间到达时,定时器立即执行对键的删除操作。
- 优点:节约内存,到时就删除,快速释放掉不必要的内存占用。
- 缺点:CPU 压力很大,无论 CPU 此时负载量多高,均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量。
- 总结:用处理器的性能换取存储空间(拿时间换空间)。
6.2.4 惰性删除
- 数据到达过期时间,不做处理,等下次访问该数据的时候,
- 如果没有过期,返回数据。
- 如果已经过期,删除数据,返回不存在。
- 优点:节约 CPU 性能,发现必须删除的时候才删除。
- 缺点:内存压力很大,出现长期占用内存的数据。
- 总结:用存储空间换取处理器性能(拿空间换时间)。
6.2.5 周期删除
- 定时删除和惰性删除这两种方案都是走的极端,那有没有折中方案?
- Redis 的周期删除方案:
- Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10 。
- 每秒钟执行 server.hz 次
serverCron()
—>databasesCron()
—>activeExpireCycle()
。 activeExpireCycle()
对每个expires[*]
逐个进行检测,每次执行 250 ms/ server.hz。- 对某个
expires[*]
检测的时候,随机挑选 W 个 key 检测:- 如果 key 超时,删除 key 。
- 如果一轮中删除的 key 的数量 >
W * 25 %
,循环该过程。 - 如果一轮中删除的 key 的数量 <=
W * 25 %
,检查下一个expires[*]
,0 ~15 循环。 - W 取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 属性值。
- 参数 current_db 用于记录
activeExpireCycle()
进入哪个expires[*]
执行。 - 如果
activeExpireCycle()
执行时间到期,下次从 current_db 继续向下执行。
- 周期性轮询 Redis 库中的时效性数据,采用随机抽样的策略,利用过期数据占比的方式控制删除频度。
- 特点:
- ① CPU 性能占用设置有峰值,检测频度可自定义设置。
- ② 内存压力不是很大,长期占用内存的冷数据会被持续清理。
- 总结:周期性检查存储空间(随机抽查、重点抽查)。
6.2.6 总结
删除策略 | 内存 | CPU | 总结 |
---|---|---|---|
定时删除 | 节约内存,无占用 | 不分时段占用 CPU 资源,频度高 | 拿时间换空间 |
惰性删除 | 内存占用严重 | 延时执行,CPU 利用率高 | 拿空间换时间 |
周期删除 | 内存定期随机清理 | 每秒花费固定的 CPU 资源维护内存 | 随机抽查、重点抽查 |
- Redis 内部使用的是
惰性删除
和周期删除
。
6.3 逐出算法
6.3.1 概述
Redis 使用内存存储数据,在执行每一条命令前,会调用
freeMemoryIfNeeded()
检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为逐出算法。注意:逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行。当对所有数据尝试完毕后,如果不能达到内存请求的要求,将出现错误信息。
(error) OOM command not allowed when used memory >'maxmemory'
6.3.2 影响输出逐出的相关配置
- 最大可使用内存:占用物理内存的比例,默认为 0 ,表示不限制。生产环境中根据需求设定,通常设置为 50 % 以上。
maxmemory
- 每次选取待删除数据的个数:选取数据的时候并不会全库扫描,导致严重的性能消耗,降低读写性能。因此采用随机获取数据的方式作为待检测删除数据
maxmemory-samples count
- 逐出算法策略:达到最大内存后,对被挑选出来的数据进行逐出算法
maxmemory-policy policy
6.3.3 逐出算法策略
- ① 检测易失数据(可能会过期的数据集server.db[i].expires ):
- volatile-lru:挑选最近最少使用的数据淘汰。
- volatile-lfu:挑选最近使用次数最少的数据淘汰。
- volatile-ttl:挑选将要过期的数据淘汰。
- volatile-random:任意选择数据淘汰。
- ② 检测全库数据(所有数据集server.db[i].dict ):
- allkeys-lru:挑选最近最少使用的数据淘汰。
- allkeys-lfu::挑选最近使用次数最少的数据淘汰。
- allkeys-random:任意选择数据淘汰,相当于随机。
- ③ 放弃数据驱逐:
- no-enviction(驱逐):禁止驱逐数据(redis 5.0 中默认策略),会引发OOM(Out Of Memory)。
- 上面的这些策略配置到那个属性上,怎么配置?
vim /usr/local/redis/redis.conf
# 新增配置
maxmemory-policy volatile-lru
- 逐出算法策略配置依据:使用 info 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据业务调优 Redis 的配置。
7 服务器基础配置 — redis.conf
7.1 服务器端配置
- 设置服务器以守护进程的方式运行:
daemonize yes | no
- 绑定主机地址:
bind 127.0.0.1
- 设置服务器端口号:
port 6379
- 设置数据库数量:
databases 16
- 保护模式:默认是 yes,即开启。关闭 protected-mode 模式,此时外部网络可以直接访问。开启 protected-mode 保护模式,需配置 bind ip 或者设置访问密码。
protected-mode yes | no
7.2 日志配置
- 设置服务器以指定日志记录级别(默认为 notice ):
loglevel debug|verbose|notice|warning
注意:日志级别,开发设置为 verbose 即可,生产环境设置为 notice ,简化日志输出量,降低写日志 IO 的频度。
- 日志记录文件名:
logfile "端口号.log"
7.3 客户端配置
- 设置同一时间最大客户端连接数,默认无限制。当客户端连接到达上限后,Redis 会关闭新的连接:
maxclients 0
- 客户端闲置等待最长时间,达到最大值后关闭连接。如需关闭该功能,设置为 0 :
timeout 0
7.4 多服务器快捷配置
- 导入并加载指定配置文件信息,用于快捷创建 Redis 公共配置较多的 Redis 实例配置文件,便于维护:
include /path/server-端口号.conf