Knowledge note
正在加载知识笔记
正在加载知识笔记
Knowledge note
## 五、Redis配置文件解读 :::info vim编辑器使用
普通模式下:
:行号 (跳转到指定行)
/abc (向下查找abc) n (跳转下一个abc)
?abc (向上查找abc)
:::
bind 决定了 Redis 站在服务器的“哪个网卡(网络接口)”后面监听请求
一个服务器通常有多个网卡(网络接口)
- 回环接口 (Loopback): IP 固定为 127.0.0.1(只能自己访问自己)。
- 局域网接口 (LAN): IP 可能是 192.168.1.5(局域网内的机器能访问)。
- 公网接口 (WAN): IP 可能是 47.100.x.x(整个互联网都能访问)。
redis默认情况下是bind 127.0.0.1(只允许本机访问)
![]()
三种配置场景
A:只允许本机访问
bind 127.0.0.1 -::1
B:允许局域网/指定网段访问
bind 192.168.136.100
若同时也想让本机访问
bind 127.0.0.1 192.168.1.5
c: 允许任意 IP 访问(最危险)
bind 0.0.0.0
# 或者直接把 bind 这一行注释掉
注意: Protected Mode (保护模式)
Redis 默认开启 protected-mode yes。它的逻辑是:
如果你既没有设置 bind(即允许全网访问),也没有设置密码(requirepass),Redis 觉得你太危险了,为了保护你,它会强制拒绝所有来自外部的连接,只允许本机访问。
如果你想开启远程连接,标准做法是:
**bind**:bind 0.0.0.0 (或者具体的内网 IP)。requirepass "你的复杂密码"。protected-mode no (如果设置了密码,这一步通常不需要,但为了保险可以关掉)。最后查看redis监听网络接口
netstat -nltp | grep 6379
允许任何网络连接
如何修改默认端口
port 16379 #将默认端口设置为16379
之后重启Redis,并且连接时也指定新的端口
redis-cli -p 16379
TCP Backlog 是操作系统内核维护的一个队列(Queue),用来存放已经完成了 TCP 三次握手,但还没有被应用程序(比如 Redis、Nginx、Tomcat)取走的连接。
一个空闲的客户端维持多少秒会关闭,0表示关闭该功能,即永不关闭
对访问客户端的一种心跳检测,每个n秒检测一次
单位为秒,如果设置为0,则不会进行检测。建议设置为60
配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit,大小写不敏感
在当前配置文件中引入一些其他的配置文件的内容,一般都是引入一些公共的配置
是否为后台进程,设置为yes(后台)守护进程,后台启动
存放PID文件的位置,每个实例会产生一个不同的pid文件
设定库的数量,默认是16,默认数据库为0,可以使用select 命令在连接上指定的数据库
1 设置密码
![]()
访问密码的查看、设置和取消
在命令中设置密码只是临时的。重启redis服务器,密码就还原了
永久设置,需要再配置文件中进行设置
requirepass 123456
auth 密码
重新连接客户端测试
这是 Redis 内存管理的终极防线。
当 Redis 的内存使用量达到 maxmemory 设置的上限时,Redis 就必须做一个艰难的决定:为了接纳新的数据,我该杀掉谁?
maxmemory-policy 就是你制定的“裁员规则”。
为了让你彻底理解,我把这 8 种策略拆解为 两个维度 的组合:“杀谁(范围)” 和 “怎么杀(算法)”。
Redis 把所有的 Key 分为两类池子,决定从哪个池子里挑人下手:
确定了范围后,用什么标准来决定谁先走:
将上面的维度组合起来,就构成了 Redis 的所有策略:
| 策略名称 | 淘汰范围 | 淘汰规则 | 应用场景 (重点) |
|---|---|---|---|
| noeviction | (默认) | 不删除 | 拒绝写入,直接报错 (OOM)。保证数据完整性,但服务会不可用。 |
| allkeys-lru | 所有 Key | 最久未用 | 最常用! 适合通用的缓存场景。 |
| allkeys-lfu | 所有 Key | 最少使用 | 适合长尾流量明显,需要保留高频热点的场景。 |
| allkeys-random | 所有 Key | 随机 | 很少用。 |
| volatile-lru | 设置了过期的 Key | 最久未用 | 适合混用场景(部分数据持久化,部分是缓存)。 |
| volatile-lfu | 设置了过期的 Key | 最少使用 | 同上,但算法更倾向于保留热点。 |
| volatile-random | 设置了过期的 Key | 随机 | 很少用。 |
| volatile-ttl | 设置了过期的 Key | 快过期的 | 希望让快过期的键优先被清理,而不是按热度清理。 |
Redis 并没有为每个 Key 维护一个精确的链表(那样太耗内存了)。
做法: 它是随机采样。比如随机挑 5 个 Key,看谁最老,删掉最老那个。
maxmemory-samples 配置采样数量(默认 5),数值越大越精确,但越耗 CPU。假设有一个数据,它是“冷门数据”,但就在刚才(1秒前)被查询了一次。
这就是 LFU (Least Frequently Used) 诞生的原因。LFU 记录的是“热度”,即使冷门数据刚才被访问了一次,它的热度还是很低,依然会被淘汰。
allkeys-lru。allkeys-lfu。noeviction(然后此时程序会报错,你需要扩容或手动清理)。volatile-lru。这样只会清理 session,不会动用户配置。需要我帮你检查一下你现在的配置是什么吗?可以使用 CONFIG GET maxmemory-policy 查看。
核心定义
Redis 事务是一组命令的集合。它允许你把多个命令打包,然后按顺序、排他性地执行。它主要保证两点:
隔理性(Serialization): 事务中的所有命令会被序列化、按顺序执行。在执行过程中,其他客户端发送的命令请求不会插入到该事务的命令序列中(即:在这个事务执行完之前,别人的命令都得在外面等着)。
原子性(有限的): 事务中的命令要么全部被执行,要么(在特定语法错误下)全部不执行。但请注意,它没有回滚机制。
| 命令 | 阶段 | 核心功能 | 对应 SQL 操作 |
|---|---|---|---|
| MULTI | 准备阶段 | 切换到事务模式,开始排队 | <font style="color:rgb(68, 71, 70);">BEGIN TRANSACTION</font> |
| EXEC | 执行阶段 | 一次性按序执行队列命令 | <font style="color:rgb(68, 71, 70);">COMMIT</font> |
| DISCARD | 撤销阶段 | 丢弃队列,退出事务模式 | <font style="color:rgb(68, 71, 70);">ROLLBACK</font>(仅限未提交前) |
| WATCH | 检查阶段 | 乐观锁,监控 Key 是否变动 | 类似 <font style="color:rgb(68, 71, 70);">SELECT ... FOR UPDATE</font> |
1. MULTI--- 开启事务。
EXEC, DISCARD, WATCH, UNWATCH 外)都不会立即执行,而是被放入一个命令队列中。OK。2. EXEC--- 执行事务。
WATCH 导致事务被取消,则返回空(Null)。3. DISCARD--- 取消事务。
OK。4. WATCH [key1] [key2] ... ---监视(乐观锁)。
MULTI 之前调用。它会监视一个或多个 Key,如果在 MULTI 到 EXEC 之间,这些被监视的 Key 被其他客户端修改了,那么接下来的 EXEC 将直接失效,不执行任何命令。5. UNWATCH---取消监视。
WATCH 之后,因为某些原因不想执行事务了,可以使用此命令释放监视。执行 EXEC 或 DISCARD 也会自动触发 UNWATCH。
在使用这些命令时,有两种不同的错误处理逻辑:
MULTI 之后输错了命令(比如 SETT 而不是 SET),Redis 会在入队时就报错。EXEC 时,Redis 会拒绝执行整个事务。LPOP)。EXEC 时,错误的命令会报错,但其他正确的命令会照常执行。Redis 不会因为其中一条报错而撤销(回滚)已经执行成功的命令。Redis中没有原生的悲观锁, 但在实际开发中,我们通常利用 Redis 的分布式锁 来“模拟”实现悲观锁的效果。
案例一:基础事务——模拟银行转账
场景: 用户 A 向用户 B 转账 100 元。我们需要保证 A 扣钱和 B 加钱这两个操作“打包”执行。
执行步骤:
MULTIEXEC代码演示:
> SET balance:A 1000
OK
> SET balance:B 500
OK
> MULTI
OK
> DECRBY balance:A 100
QUEUED
> INCRBY balance:B 100
QUEUED
> EXEC
1) (integer) 900
2) (integer) 600
分析: 在 EXEC 之前,任何其他客户端查询 A 和 B 的余额,看到的依然是 1000 和 500。只有 EXEC 后,数据才会一次性更新。
案例二:乐观锁——解决抢购“超卖”问题
场景: 有一个限量商品库存为 1。两个用户同时点击购买。如果只是简单地先 GET 再 DECR,在高并发下可能会出现库存变成 -1 的情况。
工具: WATCH 命令(乐观锁)。它监控某个 Key,如果该 Key 在事务执行前被改动了,事务将失败。
执行流程:
WATCH stockGET stock(假设结果为 1)DECR stockEXEC演示(冲突情况):
WATCH stock -> GET stock (返回1)DECR stock (库存变0)MULTI -> DECR stock -> EXECEXEC 返回 (nil)。分析: 客户端 1 的事务失败了,因为它发现 stock 被人动过了。程序此时应该捕获这个失败,并提示用户“抢购失败”或重试。
案例三:分布式锁——防止重复处理任务
场景: 多个服务器节点同时运行一个定时任务(如发送结算邮件),但我们只希望其中一台执行,避免重复发送。
工具: SET key value NX PX。
NX: 只在 Key 不存在时才执行(保证唯一性)。PX 30000: 设置 30 秒自动过期(防止程序崩溃导致锁死)。执行流程:
SET lock:task_001 "random_value" NX PX 30000
OK:说明抢到了锁,开始执行发邮件逻辑。nil:说明锁已被其他服务器占用,直接跳过。| 特性 | Redis 事务 (MULTI/EXEC) | 乐观锁 (WATCH) | 分布式锁 (SETNX) |
|---|---|---|---|
| 核心目的 | 批量执行命令,不被插队 | 保证数据在读取和修改间没被动过 | 跨服务器保证只有一个节点在操作 |
| 处理冲突 | 不处理,直接按顺序跑完 | 发现改动就直接回滚(报错) | 抢不到就等待或放弃 |
| 性能 | 高(纯内存队列) | 高(不阻塞,只检查标记) | 中(涉及网络开销和超时逻辑) |
| 适用场景 | 内部多步操作联动 | 单机并发更新同一个值 | 多机部署的资源抢占 |
场景:我要实现一个“拿号”逻辑。
GET count 看看当前是几号。count < 10,就 INCR count。count >= 10,就报错。如果你在 Java 里写这三步,高并发下一定会出问题(超卖/超发),因为这三步不是原子的。哪怕你用了 Redis 事务(Multi/Exec),也不能在事务中间根据 GET 的结果去决定是否 INCR(因为事务中间拿不到结果)。
Lua 脚本就是来救命的。
它的三大核心优势:
EVAL在 Redis 命令行或者 Java 代码中,我们使用 EVAL 命令来执行脚本。
EVAL script numkeys key [key ...] arg [arg ...]
script: Lua 代码本身。numkeys: 涉及到的 Key 的数量。key: 具体的 Key(在 Lua 里用 KEYS[1], KEYS[2] 访问)。arg: 附加参数(在 Lua 里用 ARGV[1], ARGV[2] 访问)。为什么要把 Key 和 Arg 分开?
很多同学偷懒,把 Key 直接写死在脚本字符串里,或者当成 Arg 传进去。 千万别这么做! 如果你在 Redis Cluster(集群) 模式下,Redis 需要根据 KEYS 来计算 Slot(哈希槽),从而确定把命令发给哪台机器。如果你不传 KEYS,集群就废了
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
</dependencies>
-- 获取指定 key 的当前值
-- KEYS[1] 是传入的 key(例如:一个资源的锁 key 或版本 key)
-- redis.call('GET', KEYS[1]) 返回该 key 的当前值(如果是字符串类型)
local current = redis.call('GET', KEYS[1])
-- 判断当前值是否等于期望的值(ARGV[1] 是客户端传入的“旧值”或“预期值”)
-- 例如:在乐观锁场景中,ARGV[1] 是客户端上次读取到的版本号或值
if current == ARGV[1] then
-- 如果相等,说明期间没有被其他客户端修改,可以安全更新
-- 将 key 的值设置为新值 ARGV[2](客户端传入的新值)
redis.call('SET', KEYS[1], ARGV[2])
-- 操作成功,返回 true(Lua 中 true 表示成功)
return true
end
-- 如果当前值不等于预期值,说明已经被其他客户端修改过
-- 更新失败,返回 false(表示冲突或版本不一致)
return false
/**
* Redis 相关配置类
*
* 主要目的:
* 1. 自定义 RedisTemplate 的序列化方式,使 key 可读、value 支持复杂对象
* 2. 加载 Lua 脚本并注册为 RedisScript Bean,方便后续原子执行
*/
@Configuration
public class RedisConfig {
/**
* 自定义 RedisTemplate<String, Object>
*
* Spring Boot 默认会自动配置一个 RedisTemplate,但默认序列化器是 JDK 序列化,
* 导致 key 和 value 在 Redis 中显示为二进制乱码,不便于查看和调试。
* 这里手动配置:
* - key 和 hashKey 使用 StringRedisSerializer(纯字符串,可读)
* - value 和 hashValue 使用 GenericJackson2JsonRedisSerializer(JSON 格式,可读且支持复杂对象)
*
* @param redisConnectionFactory Spring Boot 自动注入的 Redis 连接工厂
* @return 配置好的 RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建 RedisTemplate 实例,泛型指定 key 为 String,value 为 Object
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂(必须),用于建立与 Redis 服务器的连接
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 创建字符串序列化器(UTF-8 编码),用于 key 和 hashKey
StringRedisSerializer stringRedisSerializer = StringRedisSerializer.UTF_8;
// 创建 JSON 序列化器,用于 value 和 hashValue,支持 POJO、List、Map 等复杂类型
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// ==================== Key 序列化配置 ====================
redisTemplate.setKeySerializer(stringRedisSerializer); // 普通操作的 key(如 set/get)
redisTemplate.setHashKeySerializer(stringRedisSerializer); // Hash 操作的 field(即 hashKey)
// ==================== Value 序列化配置 ====================
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer); // 普通 value
// 注意:原代码这里写错了,重复设置了 hashKey,应该设置 hashValue
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer); // Hash 操作的 value
// 配置完成后,建议调用 afterPropertiesSet() 确保所有配置生效(Spring 会自动调用,通常可省略)
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 注册一个 Lua 脚本为 RedisScript Bean
*
* 作用:将 classpath 下的 Lua 脚本加载为 RedisScript 对象,
* 后续可以通过 RedisTemplate.execute() 原子性地执行该脚本。
*
* 这里加载的脚本路径为:src/main/resources/luas/test.lua
* 返回值类型指定为 Boolean,表示脚本执行后返回 true/false
*
* @return RedisScript<Boolean>
*/
@Bean
public RedisScript<Boolean> booleanRedisScript() {
// 从 classpath 下加载 Lua 脚本文件
// 路径相对为 luas/test.lua,即 resources/luas/test.lua
Resource resource = new ClassPathResource("luas/test.lua");
// 创建 RedisScript 实例,指定脚本资源和返回值类型
return RedisScript.of(resource, Boolean.class);
}
}
/**
* Redis Lua 脚本原子操作测试类
*
* 测试目标:验证通过 Lua 脚本实现的 “比较当前值并设置新值” (Compare And Set) 操作是否原子且正确。
* 这是一种常见的乐观锁实现方式,用于防止并发修改时的数据丢失。
*
* @SpringBootTest(classes = Main.class)
* - 指定只加载 Main 启动类所在的上下文,避免加载整个项目的所有配置(启动更快)
* - 如果你的启动类叫其他名字(如 Application),请相应修改
*/
@SpringBootTest(classes = Main.class) // 替换 Main.class 为你的 Spring Boot 启动类
public class RedisTest {
// 注入之前在 RedisConfig 中自定义的 RedisTemplate<String, Object>
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 注入之前在 RedisConfig 中注册的 Lua 脚本 RedisScript<Boolean>
@Autowired
private RedisScript<Boolean> redisScript; // 默认 Bean 名为 booleanRedisScript,可改名或用 @Qualifier 指定
/**
* 测试原子性比较并设置操作
*/
@Test
public void test() {
// 1. 先往 Redis 中存入初始值:key = "admin", value = "123456"
// 注意:由于我们配置了 GenericJackson2JsonRedisSerializer,字符串也会被当成对象处理,但仍能正常存取
redisTemplate.opsForValue().set("admin", "123456");
// 2. 准备 Lua 脚本所需的 KEYS 和 ARGV
// KEYS[1]:要操作的 key,即 "admin"
List<String> keys = List.of("admin");
// ARGV[1]:期望的旧值 "123456"
// ARGV[2]:要设置的新值 "654321"
// 3. 执行 Lua 脚本(原子操作)
// execute 参数说明:
// - redisScript:加载的 Lua 脚本(返回 Boolean 类型)
// - keys:List<String> 类型,对应脚本中的 KEYS[1], KEYS[2]...
// - args:可变参数,对应脚本中的 ARGV[1], ARGV[2]...
Boolean b = redisTemplate.execute(redisScript, keys, "123456", "654321");
// 4. 根据脚本返回值判断是否修改成功
if (b) {
// 当前值确实是 "123456",没有被其他人修改,成功更新为 "654321"
System.out.println("修改成功");
// 验证一下新值
Object newValue = redisTemplate.opsForValue().get("admin");
System.out.println("当前值:" + newValue); // 预期输出:654321
} else {
// 当前值已经不是 "123456"(被其他线程/请求修改过),更新失败
System.out.println("修改失败");
// 实际场景中这里应该重新读取最新值,然后重试业务逻辑
}
}
}
为什么要持久化?😎
:::tips
:::
这种方式Redis会定期在磁盘上生成一个快照,类似把内存上所有的数据都拍了个照存在磁盘上,后续重启在读取这个快照就能恢复数据
怎么定期生成?
:::tips 手动提交和自动提交:
**手动提交:**save命令,他会阻塞Redis服务,不能处理其他客户端的请求(Redis是单线程服务),数据量过大阻塞时间就越久,一般不建议手动提交。
**自动提交:**bgsave,他会根据操作次数/时间间隔来自动提交,执行的时候不会阻塞Redis服务器,而是采用多进程的方式并发处理。
:::
下面来看一张bgsave的执行流程图:
1:执行bgsave发送到Redis服务器,父进程查看已经有子进程在进行生成快照,则直接返回。
2:生成子进程执行生成快照操作。
3:同时父进程继续处理其他客户端请求。
4/5:生成快照完文件告诉父进程然后退出销毁。
// 默认在该目录下
/etc/redis/redis.conf
// 找到这个路径
dir /var/lib/redis
// 该目录下有个dump.rdb文件就是生成的快照文件
// 修改生成的快照文件名
dbfilename dump.rdb
// 配置自动提交的参数
// 左边秒为单位,右边修改次数,必须同时满足才会触发,重启服务生效
save 900 1
save 300 10
save 60 10000
// save "" 表示禁止自动生成
3.1 该文件储存的是二进制数据。
3.2 可以用 redis-check-rdb 检测文件是否损坏。
通过上述的自动生成的参数,假设60s生成一次,在60s之内这个间隙,如果Redis服务挂了(异常情况),内存中的实时数据就无法同步到磁盘上了,就有丢失数据的风险。如果把间隔设置很短,就会大量的 生成快照和磁盘IO成本开销大。AOF就是来解决这个问题的。
4 触发生成快照的方式
:::tips 优缺点:
:::
// 如果开启AOF,RDB自动触发不生效
// 找到配置文件,一般是没开启的
appendonly no
// 可以修改生成的文件名,默认的工作目录和RDB一样
appendfilename "appendonly.aof"
:::tips
Redis默认采用的是以秒为刷新
:::
:::tips set key 1 ,set key 2,set key 3,最终结果就是 set key 3
lpush key 1,lpush key 2 ,lpush key 3 最终结果就是 lpush key 1 2 3
set key 1 ,del key,set key 2,del key 最终结果什么也不干
:::
针对上述操作,有的操作是冗余的比如插入在删除,插入覆盖之前的key,把分3次插入合并成一次插入操作,这样就可以减少不必要的资源开销,还能让文件只存有效数据达到文件大小变小,这种策略就是AOF的重写机制,而缓冲区存放的是最终的数据结果,就直接避免了这些不必要的操作。
AOF文件默认是以文本形式存放的,RDB则是二进制文件,当数据量越来越多,AOF加载文件就会很慢,所以Redis采用了AOF和RDB的优点进行了混合持久化,AOF重写的时候按RDB形式写,后续新来的数据按AOF形式写,最终新的AOF文件既有二进制也有文本。通过配置文件进行配置。
// 配置文件选择开启
aof-use-rdb-preamble yes
二 对比RDB/AOF
:::tips RDB和AOF的实现机类似
RDB主要用来定时备份,AOF主要用来实时备份,AOF的数据比RDB更新
RDB记录的数据快照,AOF记录的数据执行的命令
RDB文件格式是二进制,AOF则是文本或者混合持久化文本+二进制,RDB恢复数据坏,数据体积小
RDB 2次快照之间的数据可能丢失,RDB则会把新的数据继续缓存起来并同步到文件
:::
知识笔记会随着实践和认知变化持续更新,不代表最终结论。