Redis 从入门到精通:Spring Boot 实战三部曲(二)—— 进阶原理与高可用架构

Redis 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶原理与高可用架构

专题导读:本系列共三篇,带你系统掌握 Redis 在 Spring Boot 项目中的实战应用。

  • 第一篇 基础核心与快速上手
  • 第二篇:进阶原理与高可用架构(本文)
  • 第三篇 高级特性与性能优化

📖 前言

在上一篇文章中,我们学习了 Redis 的五大数据结构及其在 Spring Boot 中的应用。本文将深入探讨 Redis 的核心原理,包括持久化机制、主从复制、哨兵模式等高级特性,帮助你构建高可用的 Redis 架构。

学完本文你将掌握:

  • ✅ Redis 持久化机制(RDB & AOF)的原理与配置
  • ✅ 主从复制的工作流程与搭建方法
  • ✅ 哨兵模式实现高可用
  • ✅ 事务与 Lua 脚本的原子操作
  • ✅ 分布式锁的高级实现
  • ✅ 生产环境的最佳实践

一、Redis 持久化机制深度解析

1.1 为什么需要持久化?

Redis 是基于内存的数据库,虽然速度极快,但断电后数据会丢失。持久化机制可以将内存中的数据保存到磁盘,保证数据的可靠性。

持久化的两大场景:

  1. 数据备份:定期保存数据快照
  2. 故障恢复:服务重启后恢复数据

1.2 RDB(Redis Database)快照

工作原理

RDB 是 Redis 默认的持久化方式,通过创建内存快照来保存数据。

复制代码
触发 RDB → Fork 子进程 → 子进程遍历内存生成 RDB 文件 → 
替换旧文件 → 通知主进程完成

流程图:

复制代码
┌──────────┐      ┌──────────┐      ┌──────────┐
│ 主进程    │      │ 子进程    │      │ 磁盘      │
│          │      │          │      │          │
│ 继续处理  │─────►│ 遍历内存  │─────►│ 写入RDB  │
│ 客户端请求│      │ 生成快照  │      │   文件    │
└──────────┘      └──────────┘      └──────────┘
触发方式

1. 自动触发(配置文件)

conf 复制代码
# redis.conf
save 900 1      # 900秒内至少1个key变化
save 300 10     # 300秒内至少10个key变化
save 60 10000   # 60秒内至少10000个key变化

2. 手动触发

bash 复制代码
# 阻塞式(不推荐生产环境使用)
SAVE

# 非阻塞式(推荐)
BGSAVE
配置详解
conf 复制代码
# RDB 文件名
dbfilename dump.rdb

# RDB 文件存储目录
dir /var/lib/redis

# RDB 压缩
rdbcompression yes

# RDB 校验和
rdbchecksum yes

# 停止写入时是否接受写操作
stop-writes-on-bgsave-error yes
优缺点分析
优点 缺点
✅ 文件紧凑,适合备份 ❌ 可能丢失最后一次快照的数据
✅ 恢复速度快 ❌ Fork 子进程消耗内存和 CPU
✅ 最大化 Redis 性能 ❌ 大数据集时 Fork 耗时较长
✅ 适合灾难恢复 ❌ 无法实现实时持久化
Spring Boot 监控 RDB 状态
java 复制代码
@Service
@Slf4j
public class RedisMonitorService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取 RDB 持久化信息
     */
    public Map<String, Object> getRdbInfo() {
        Properties info = redisTemplate.getConnectionFactory()
            .getConnection().info("persistence");
        
        Map<String, Object> rdbInfo = new HashMap<>();
        rdbInfo.put("rdb_bgsave_in_progress", 
            info.getProperty("rdb_bgsave_in_progress"));
        rdbInfo.put("rdb_last_bgsave_status", 
            info.getProperty("rdb_last_bgsave_status"));
        rdbInfo.put("rdb_last_bgsave_time_sec", 
            info.getProperty("rdb_last_bgsave_time_sec"));
        rdbInfo.put("rdb_current_bgsave_time_sec", 
            info.getProperty("rdb_current_bgsave_time_sec"));
        rdbInfo.put("rdb_last_save_timestamp", 
            info.getProperty("rdb_last_save_timestamp"));
        
        return rdbInfo;
    }

    /**
     * 手动触发 BGSAVE
     */
    public void triggerBgSave() {
        redisTemplate.execute((RedisCallback<Void>) connection -> {
            connection.bgSave();
            return null;
        });
        log.info("已触发 BGSAVE");
    }
}

1.3 AOF(Append Only File)追加日志

工作原理

AOF 通过记录每个写命令来实现持久化,重启时重新执行这些命令恢复数据。

复制代码
写命令 → 追加到 AOF 缓冲区 → 根据策略同步到磁盘

三种同步策略:

策略 说明 性能 安全性
always 每条命令都同步 最安全
everysec 每秒同步一次 适中 推荐
no 由 OS 决定 最不安全
配置详解
conf 复制代码
# 启用 AOF
appendonly yes

# AOF 文件名
appendfilename "appendonly.aof"

# 同步策略(推荐 everysec)
appendfsync everysec

# AOF 重写配置
auto-aof-rewrite-percentage 100  # 增长100%时触发重写
auto-aof-rewrite-min-size 64mb   # 最小64MB才重写

# AOF 加载时是否检查完整性
aof-load-truncated yes

# 混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes
AOF 重写机制

为什么需要重写?

AOF 文件会随着写操作不断增长,重写可以压缩文件大小。

复制代码
原始 AOF:                          重写后 AOF:
SET key1 value1                    SET key1 value3
SET key1 value2         →         SET key2 value4
SET key1 value3                    
SET key2 value4                    

重写流程:

复制代码
1. 主进程 fork 子进程
2. 子进程根据当前内存状态生成新 AOF
3. 主进程累积新写入命令到 AOF buffer
4. 子进程完成后,主进程将 buffer 内容追加到新 AOF
5. 原子替换旧 AOF 文件
优缺点分析
优点 缺点
✅ 数据更安全(最多丢失1秒) ❌ 文件体积通常比 RDB 大
✅ 易于理解和维护 ❌ 恢复速度比 RDB 慢
✅ 支持实时持久化 ❌ 相同数据集下 AOF 比 RDB 大
✅ AOF 文件损坏可修复 ❌ 重写时消耗资源
Spring Boot AOF 管理
java 复制代码
@Service
@Slf4j
public class AofManagementService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取 AOF 信息
     */
    public Map<String, Object> getAofInfo() {
        Properties info = redisTemplate.getConnectionFactory()
            .getConnection().info("persistence");
        
        Map<String, Object> aofInfo = new HashMap<>();
        aofInfo.put("aof_enabled", info.getProperty("aof_enabled"));
        aofInfo.put("aof_rewrite_in_progress", 
            info.getProperty("aof_rewrite_in_progress"));
        aofInfo.put("aof_last_rewrite_time_sec", 
            info.getProperty("aof_last_rewrite_time_sec"));
        aofInfo.put("aof_current_rewrite_time_sec", 
            info.getProperty("aof_current_rewrite_time_sec"));
        aofInfo.put("aof_last_bgrewrite_status", 
            info.getProperty("aof_last_bgrewrite_status"));
        aofInfo.put("aof_current_size", 
            info.getProperty("aof_current_size"));
        aofInfo.put("aof_base_size", 
            info.getProperty("aof_base_size"));
        
        return aofInfo;
    }

    /**
     * 手动触发 AOF 重写
     */
    public void triggerAofRewrite() {
        redisTemplate.execute((RedisCallback<Void>) connection -> {
            connection.bgRewriteAof();
            return null;
        });
        log.info("已触发 AOF 重写");
    }

    /**
     * 切换 AOF 同步策略
     */
    public void setAofSyncPolicy(String policy) {
        // policy: always, everysec, no
        redisTemplate.execute((RedisCallback<Void>) connection -> {
            connection.setConfig("appendfsync", policy);
            return null;
        });
        log.info("AOF 同步策略已设置为: {}", policy);
    }
}

1.4 RDB vs AOF 如何选择?

对比总结
维度 RDB AOF
数据安全性 较低(可能丢失较多) 较高(最多丢失1秒)
文件大小 小(压缩快照) 大(记录所有命令)
恢复速度
性能影响 Fork 时短暂停顿 持续写入开销
适用场景 备份、容忍部分数据丢失 对数据安全性要求高
推荐方案

生产环境最佳实践:同时启用 RDB + AOF

conf 复制代码
# 同时启用 RDB 和 AOF
save 900 1
save 300 10
save 60 10000

appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes  # 混合持久化

优势:

  • Redis 重启时优先使用 AOF 恢复数据(更完整)
  • AOF 文件过大时可以通过 RDB 重写压缩
  • 兼顾数据安全性和恢复速度

二、主从复制(Master-Slave)

2.1 主从复制架构

复制代码
┌──────────────┐
│   Master     │  ← 写操作
│ 192.168.1.1  │
└──────┬───────┘
       │
       ├──────────┬──────────┐
       ▼          ▼          ▼
  ┌─────────┐ ┌─────────┐ ┌─────────┐
  │ Slave 1 │ │ Slave 2 │ │ Slave 3 │  ← 读操作
  │  .1.2   │ │  .1.3   │ │  .1.4   │
  └─────────┘ └─────────┘ └─────────┘

特点:

  • 一个 Master 可以有多个 Slave
  • Master 负责写,Slave 负责读(读写分离)
  • Slave 可以级联连接(Slave of Slave)

2.2 复制流程详解

完整复制过程
复制代码
1️⃣ 建立连接
   Slave 发送 SYNC 命令到 Master

2️⃣ 主库生成 RDB
   Master 执行 BGSAVE 生成 RDB 文件

3️⃣ 传输 RDB
   Master 将 RDB 文件发送给 Slave

4️⃣ 从库加载
   Slave 清空旧数据,加载 RDB 文件

5️⃣ 增量同步
   Master 持续发送写命令给 Slave
全量复制 vs 增量复制

全量复制触发条件:

  • Slave 首次连接 Master
  • Slave 断线时间过长,repl_backlog_buffer 被覆盖
  • 手动执行 SLAVEOF 命令

增量复制:

  • 短时间断线重连
  • 通过 repl_backlog_buffer 同步缺失的命令

repl_backlog_buffer 配置:

conf 复制代码
# 复制积压缓冲区大小
repl-backlog-size 1mb

# 缓冲区超时时间(秒)
repl-backlog-ttl 3600

2.3 搭建主从环境

方式一:配置文件

Master 配置(redis-master.conf):

conf 复制代码
bind 0.0.0.0
port 6379
requirepass master_password
masterauth master_password

Slave 配置(redis-slave-1.conf):

conf 复制代码
bind 0.0.0.0
port 6380
requirepass slave_password
masterauth master_password
slaveof 192.168.1.1 6379
slave-read-only yes

启动:

bash 复制代码
# 启动 Master
redis-server redis-master.conf

# 启动 Slave
redis-server redis-slave-1.conf
redis-server redis-slave-2.conf
方式二:命令行动态配置
bash 复制代码
# 连接到 Slave 节点
redis-cli -p 6380

# 设置 Master
127.0.0.1:6380> SLAVEOF 192.168.1.1 6379
OK

# 设置 Master 密码
127.0.0.1:6380> CONFIG SET masterauth master_password
OK

2.4 监控主从状态

bash 复制代码
# 查看复制信息
redis-cli INFO replication

# 输出示例:
# role:master
# connected_slaves:2
# slave0:ip=192.168.1.2,port=6380,state=online,offset=12345,lag=1
# slave1:ip=192.168.1.3,port=6381,state=online,offset=12340,lag=1
Spring Boot 监控主从状态
java 复制代码
@Service
@Slf4j
public class ReplicationMonitorService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取复制信息
     */
    public Map<String, Object> getReplicationInfo() {
        Properties info = redisTemplate.getConnectionFactory()
            .getConnection().info("replication");
        
        Map<String, Object> replInfo = new HashMap<>();
        replInfo.put("role", info.getProperty("role"));
        replInfo.put("connected_slaves", info.getProperty("connected_slaves"));
        replInfo.put("master_host", info.getProperty("master_host"));
        replInfo.put("master_port", info.getProperty("master_port"));
        replInfo.put("master_link_status", info.getProperty("master_link_status"));
        replInfo.put("slave_read_only", info.getProperty("slave_read_only"));
        
        // 解析 Slave 列表
        List<Map<String, String>> slaves = new ArrayList<>();
        int i = 0;
        while (info.containsKey("slave" + i)) {
            String slaveInfo = info.getProperty("slave" + i);
            Map<String, String> slaveMap = parseSlaveInfo(slaveInfo);
            slaves.add(slaveMap);
            i++;
        }
        replInfo.put("slaves", slaves);
        
        return replInfo;
    }

    private Map<String, String> parseSlaveInfo(String slaveInfo) {
        Map<String, String> map = new HashMap<>();
        String[] pairs = slaveInfo.split(",");
        for (String pair : pairs) {
            String[] kv = pair.split("=");
            if (kv.length == 2) {
                map.put(kv[0], kv[1]);
            }
        }
        return map;
    }

    /**
     * 检查主从同步状态
     */
    public boolean checkReplicationHealth() {
        Properties info = redisTemplate.getConnectionFactory()
            .getConnection().info("replication");
        
        String role = info.getProperty("role");
        if ("master".equals(role)) {
            String linkStatus = info.getProperty("master_link_status");
            return "up".equals(linkStatus);
        }
        
        return true;
    }
}

2.5 读写分离实现

Spring Boot 配置多数据源
java 复制代码
@Configuration
public class RedisReadWriteConfig {

    @Bean
    public RedisTemplate<String, Object> masterRedisTemplate(
            RedisConnectionFactory masterFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(masterFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    @Bean
    public RedisTemplate<String, Object> slaveRedisTemplate(
            RedisConnectionFactory slaveFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(slaveFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}
读写分离服务
java 复制代码
@Service
public class ReadWriteSeparationService {
    
    @Autowired
    @Qualifier("masterRedisTemplate")
    private RedisTemplate<String, Object> masterTemplate;
    
    @Autowired
    @Qualifier("slaveRedisTemplate")
    private RedisTemplate<String, Object> slaveTemplate;

    /**
     * 写操作(使用 Master)
     */
    public void writeData(String key, Object value) {
        masterTemplate.opsForValue().set(key, value);
    }

    /**
     * 读操作(使用 Slave)
     */
    public Object readData(String key) {
        return slaveTemplate.opsForValue().get(key);
    }

    /**
     * 删除操作(使用 Master)
     */
    public void deleteData(String key) {
        masterTemplate.delete(key);
    }
}

三、哨兵模式(Sentinel)

3.1 为什么需要哨兵?

主从模式的缺陷:

  • Master 宕机后需要手动切换
  • 无法自动故障转移
  • 不适合高可用场景

哨兵的作用:

  1. 监控(Monitoring):定期检查主从节点是否存活
  2. 通知(Notification):故障时通过 API 通知管理员
  3. 自动故障转移(Automatic Failover):选举新的 Master

3.2 哨兵工作原理

故障检测流程
复制代码
1️⃣ 主观下线(SDOWN)
   单个哨兵认为 Master 不可达

2️⃣ 客观下线(ODOWN)
   多数哨兵确认 Master 不可达

3️⃣ Leader 选举
   Raft 算法选出负责故障转移的 Leader

4️⃣ 选择新 Master
   优先级 → 复制偏移量 → 运行 ID

5️⃣ 切换从库
   其他 Slave 指向新 Master

6️⃣ 通知客户端
   更新 Master 地址
架构图
复制代码
┌──────────┐    ┌──────────┐    ┌──────────┐
│Sentinel 1│    │Sentinel 2│    │Sentinel 3│
└────┬─────┘    └────┬─────┘    └────┬─────┘
     │               │               │
     └───────────────┼───────────────┘
                     │
     ┌───────────────┼───────────────┐
     ▼               ▼               ▼
┌─────────┐    ┌─────────┐    ┌─────────┐
│ Master  │    │ Slave 1 │    │ Slave 2 │
│  .1.1   │    │  .1.2   │    │  .1.3   │
└─────────┘    └─────────┘    └─────────┘

3.3 搭建哨兵集群

哨兵配置(sentinel.conf)
conf 复制代码
# 监听端口
port 26379

# 守护进程模式
daemonize yes

# 日志文件
logfile "/var/log/redis/sentinel.log"

# 工作目录
dir /tmp

# 监控 Master(最后参数为法定票数)
sentinel monitor mymaster 192.168.1.1 6379 2

# Master 密码
sentinel auth-pass mymaster master_password

# 判断 Master 下线的时间(毫秒)
sentinel down-after-milliseconds mymaster 30000

# 故障转移超时时间(毫秒)
sentinel failover-timeout mymaster 180000

# 并行同步的 Slave 数量
sentinel parallel-syncs mymaster 1

# 通知脚本(可选)
sentinel notification-script mymaster /var/redis/notify.sh
启动哨兵
bash 复制代码
# 启动三个哨兵节点
redis-sentinel sentinel-1.conf
redis-sentinel sentinel-2.conf
redis-sentinel sentinel-3.conf

# 查看哨兵状态
redis-cli -p 26379
127.0.0.1:26379> INFO sentinel

3.4 Spring Boot 集成哨兵

application.yml 配置
yaml 复制代码
spring:
  redis:
    password: your_password
    sentinel:
      master: mymaster
      nodes:
        - 192.168.1.1:26379
        - 192.168.1.2:26379
        - 192.168.1.3:26379
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
        max-wait: 3000ms
配置类
java 复制代码
@Configuration
public class SentinelRedisConfig {

    @Value("${spring.redis.password}")
    private String password;
    
    @Value("${spring.redis.sentinel.master}")
    private String masterName;
    
    @Value("${spring.redis.sentinel.nodes}")
    private List<String> sentinelNodes;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 哨兵配置
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
        sentinelConfig.setMaster(masterName);
        
        // 添加哨兵节点
        for (String node : sentinelNodes) {
            String[] hostPort = node.split(":");
            sentinelConfig.sentinel(hostPort[0], Integer.parseInt(hostPort[1]));
        }
        
        // 密码配置
        if (StringUtils.hasText(password)) {
            sentinelConfig.setPassword(RedisPassword.of(password));
        }
        
        // 连接池配置
        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxTotal(20);
        poolConfig.setMaxIdle(10);
        poolConfig.setMinIdle(5);
        poolConfig.setMaxWaitMillis(3000);
        
        LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
            .poolConfig(poolConfig)
            .commandTimeout(Duration.ofSeconds(5))
            .build();
        
        return new LettuceConnectionFactory(sentinelConfig, clientConfig);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}
测试故障转移
java 复制代码
@SpringBootTest
@Slf4j
public class SentinelTest {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    public void testFailover() throws InterruptedException {
        // 1. 写入数据
        redisTemplate.opsForValue().set("test:key", "test:value");
        log.info("数据写入成功");
        
        // 2. 模拟 Master 宕机(手动停止 Master)
        log.info("请手动停止 Master 节点...");
        Thread.sleep(60000); // 等待故障转移
        
        // 3. 读取数据(应该能正常读取)
        Object value = redisTemplate.opsForValue().get("test:key");
        log.info("故障转移后读取数据: {}", value);
        
        // 4. 验证新 Master
        Properties info = redisTemplate.getConnectionFactory()
            .getConnection().info("replication");
        log.info("当前角色: {}", info.getProperty("role"));
    }
}

四、事务与 Lua 脚本

4.1 Redis 事务

基本命令
bash 复制代码
MULTI      # 开启事务
EXEC       # 执行事务
DISCARD    # 取消事务
WATCH key  # 监视 key(乐观锁)
UNWATCH    # 取消监视
事务示例
bash 复制代码
127.0.0.1:6379> MULTI
OK

127.0.0.1:6379> SET key1 value1
QUEUED

127.0.0.1:6379> SET key2 value2
QUEUED

127.0.0.1:6379> INCR counter
QUEUED

127.0.0.1:6379> EXEC
1) OK
2) OK
3) (integer) 1
特点与限制

特点:

  • ✅ 批量执行命令,减少网络往返
  • ✅ 隔离性:事务执行期间不会被其他命令插入
  • 不支持原子性:某个命令失败,其他命令仍会执行
  • 不支持回滚:没有 ROLLBACK 命令

WATCH 实现乐观锁:

bash 复制代码
127.0.0.1:6379> WATCH balance
OK

127.0.0.1:6379> MULTI
OK

127.0.0.1:6379> DECRBY balance 100
QUEUED

127.0.0.1:6379> INCRBY other_balance 100
QUEUED

# 如果 balance 在 WATCH 后被其他客户端修改,EXEC 会失败
127.0.0.1:6379> EXEC
(nil)  # 返回 nil 表示事务失败
Spring Boot 事务示例
java 复制代码
@Service
public class TransactionService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 使用 SessionCallback 执行事务
     */
    public void executeTransaction() {
        List<Object> results = redisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
                // 开启事务
                operations.multi();
                
                // 批量操作
                operations.opsForValue().set("key1", "value1");
                operations.opsForValue().set("key2", "value2");
                operations.opsForValue().increment("counter");
                
                // 执行事务
                return operations.exec();
            }
        });
        
        log.info("事务执行结果: {}", results);
    }

    /**
     * 使用 WATCH 实现乐观锁
     */
    public boolean transferBalance(String fromAccount, String toAccount, double amount) {
        String fromKey = "balance:" + fromAccount;
        String toKey = "balance:" + toAccount;
        
        while (true) {
            try {
                // 监视账户余额
                redisTemplate.watch(fromKey);
                
                Double fromBalance = (Double) redisTemplate.opsForValue().get(fromKey);
                if (fromBalance == null || fromBalance < amount) {
                    redisTemplate.unwatch();
                    return false;
                }
                
                // 开启事务
                Boolean result = redisTemplate.execute(new SessionCallback<Boolean>() {
                    @Override
                    public Boolean execute(RedisOperations operations) throws DataAccessException {
                        operations.multi();
                        operations.opsForValue().decrement(fromKey, amount);
                        operations.opsForValue().increment(toKey, amount);
                        List<Object> execResult = operations.exec();
                        
                        // 如果 execResult 为 null,说明事务失败(WATCH 检测到变化)
                        return execResult != null;
                    }
                });
                
                if (Boolean.TRUE.equals(result)) {
                    return true;
                }
                
                // 事务失败,重试
                log.warn("事务冲突,重试...");
                
            } catch (Exception e) {
                log.error("转账失败", e);
                return false;
            }
        }
    }
}

4.2 Lua 脚本

为什么需要 Lua?

Redis 事务的局限性:

  • 不支持条件判断
  • 不支持循环
  • 无法保证复杂操作的原子性

Lua 脚本的优势:

  • ✅ 原子执行:整个脚本作为一个整体执行
  • ✅ 减少网络往返:一次性发送多个命令
  • ✅ 灵活编程:支持条件判断、循环等
基本命令
bash 复制代码
# 执行脚本
EVAL script numkeys key [key ...] arg [arg ...]

# 缓存脚本(通过 SHA1 调用)
SCRIPT LOAD script
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
示例 1:原子性计数器
lua 复制代码
-- limit_counter.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]

local current = tonumber(redis.call('get', key) or "0")

if current + 1 > limit then
    return 0
else
    redis.call('incr', key)
    redis.call('expire', key, expire_time)
    return 1
end

Java 调用:

java 复制代码
@Service
public class LuaScriptService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 限流器(Lua 脚本实现)
     */
    public boolean rateLimit(String key, int limit, int expireSeconds) {
        String luaScript = 
            "local key = KEYS[1]\n" +
            "local limit = tonumber(ARGV[1])\n" +
            "local expire_time = ARGV[2]\n" +
            "\n" +
            "local current = tonumber(redis.call('get', key) or \"0\")\n" +
            "\n" +
            "if current + 1 > limit then\n" +
            "    return 0\n" +
            "else\n" +
            "    redis.call('incr', key)\n" +
            "    redis.call('expire', key, expire_time)\n" +
            "    return 1\n" +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(
            script, 
            Collections.singletonList(key), 
            String.valueOf(limit), 
            String.valueOf(expireSeconds)
        );
        
        return result != null && result == 1;
    }
}
示例 2:分布式锁释放
lua 复制代码
-- release_lock.lua
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

Java 调用:

java 复制代码
@Component
public class DistributedLock {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String RELEASE_LOCK_SCRIPT = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";

    /**
     * 尝试获取锁
     */
    public boolean tryLock(String key, String requestId, long expireMs) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue()
                .setIfAbsent(key, requestId, expireMs, TimeUnit.MILLISECONDS)
        );
    }

    /**
     * 释放锁(Lua 脚本保证原子性)
     */
    public boolean releaseLock(String key, String requestId) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(RELEASE_LOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(
            script, 
            Collections.singletonList(key), 
            requestId
        );
        return result != null && result == 1;
    }
}
示例 3:批量操作
lua 复制代码
-- batch_delete.lua
local keys = KEYS
local count = 0

for i, key in ipairs(keys) do
    redis.call('del', key)
    count = count + 1
end

return count

Java 调用:

java 复制代码
public Long batchDelete(List<String> keys) {
    String luaScript = 
        "local keys = KEYS\n" +
        "local count = 0\n" +
        "\n" +
        "for i, key in ipairs(keys) do\n" +
        "    redis.call('del', key)\n" +
        "    count = count + 1\n" +
        "end\n" +
        "\n" +
        "return count";
    
    DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
    return redisTemplate.execute(script, keys);
}

五、分布式锁高级实现

5.1 基础分布式锁

java 复制代码
@Component
@Slf4j
public class SimpleDistributedLock {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String LOCK_PREFIX = "lock:";

    /**
     * 获取锁
     */
    public boolean lock(String key, String requestId, long expireMs) {
        String lockKey = LOCK_PREFIX + key;
        
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue()
                .setIfAbsent(lockKey, requestId, expireMs, TimeUnit.MILLISECONDS)
        );
    }

    /**
     * 释放锁
     */
    public boolean unlock(String key, String requestId) {
        String lockKey = LOCK_PREFIX + key;
        
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(
            script, 
            Collections.singletonList(lockKey), 
            requestId
        );
        
        return result != null && result == 1;
    }
}

5.2 可重入锁

java 复制代码
@Component
public class ReentrantLock {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String LOCK_PREFIX = "reentrant:lock:";

    /**
     * 获取可重入锁
     */
    public boolean lock(String key, String requestId, long expireMs) {
        String lockKey = LOCK_PREFIX + key;
        
        // 检查是否已持有锁
        Object existingValue = redisTemplate.opsForValue().get(lockKey);
        if (requestId.equals(existingValue)) {
            // 重入,延长锁时间
            redisTemplate.expire(lockKey, expireMs, TimeUnit.MILLISECONDS);
            return true;
        }
        
        // 尝试获取新锁
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue()
                .setIfAbsent(lockKey, requestId, expireMs, TimeUnit.MILLISECONDS)
        );
    }

    /**
     * 释放锁
     */
    public boolean unlock(String key, String requestId) {
        String lockKey = LOCK_PREFIX + key;
        
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(
            script, 
            Collections.singletonList(lockKey), 
            requestId
        );
        
        return result != null && result == 1;
    }
}

5.3 看门狗机制(自动续期)

java 复制代码
@Component
@Slf4j
public class WatchDogLock {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String LOCK_PREFIX = "watchdog:lock:";
    private static final long DEFAULT_LEASE_TIME = 30000; // 30秒
    private static final long RENEWAL_INTERVAL = 10000;   // 10秒续期
    
    private ConcurrentHashMap<String, ScheduledFuture<?>> renewalTasks = new ConcurrentHashMap<>();

    /**
     * 获取锁并启动看门狗
     */
    public boolean lockWithWatchDog(String key, String requestId) {
        String lockKey = LOCK_PREFIX + key;
        
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, requestId, DEFAULT_LEASE_TIME, TimeUnit.MILLISECONDS);
        
        if (Boolean.TRUE.equals(locked)) {
            // 启动看门狗线程
            startWatchDog(lockKey, requestId);
            return true;
        }
        
        return false;
    }

    /**
     * 启动看门狗
     */
    private void startWatchDog(String lockKey, String requestId) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        
        ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
            try {
                // 检查锁是否仍然持有
                Object value = redisTemplate.opsForValue().get(lockKey);
                if (requestId.equals(value)) {
                    // 续期
                    redisTemplate.expire(lockKey, DEFAULT_LEASE_TIME, TimeUnit.MILLISECONDS);
                    log.debug("锁续期成功: {}", lockKey);
                } else {
                    // 锁已释放,停止看门狗
                    stopWatchDog(lockKey);
                }
            } catch (Exception e) {
                log.error("看门狗续期失败", e);
            }
        }, RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS);
        
        renewalTasks.put(lockKey, future);
    }

    /**
     * 停止看门狗
     */
    private void stopWatchDog(String lockKey) {
        ScheduledFuture<?> future = renewalTasks.remove(lockKey);
        if (future != null) {
            future.cancel(false);
        }
    }

    /**
     * 释放锁
     */
    public boolean unlock(String key, String requestId) {
        String lockKey = LOCK_PREFIX + key;
        
        // 停止看门狗
        stopWatchDog(lockKey);
        
        // 释放锁
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "   return redis.call('del', KEYS[1]) " +
            "else " +
            "   return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = redisTemplate.execute(
            script, 
            Collections.singletonList(lockKey), 
            requestId
        );
        
        return result != null && result == 1;
    }
}

5.4 实战案例:防止超卖

java 复制代码
@Service
@Slf4j
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private WatchDogLock distributedLock;
    
    @Autowired
    private ProductMapper productMapper;
    
    @Autowired
    private OrderMapper orderMapper;

    /**
     * 秒杀下单
     */
    @Transactional
    public boolean seckill(Long productId, Long userId) {
        String lockKey = "seckill:product:" + productId;
        String requestId = UUID.randomUUID().toString();
        
        try {
            // 1. 获取分布式锁
            if (!distributedLock.lockWithWatchDog(lockKey, requestId)) {
                throw new RuntimeException("系统繁忙,请稍后重试");
            }
            
            // 2. 查询库存
            Product product = productMapper.selectById(productId);
            if (product == null || product.getStock() <= 0) {
                log.warn("商品库存不足: productId={}", productId);
                return false;
            }
            
            // 3. 扣减库存
            product.setStock(product.getStock() - 1);
            productMapper.updateById(product);
            
            // 4. 创建订单
            Order order = new Order();
            order.setProductId(productId);
            order.setUserId(userId);
            order.setCreateTime(new Date());
            orderMapper.insert(order);
            
            log.info("秒杀成功: productId={}, userId={}", productId, userId);
            return true;
            
        } finally {
            // 5. 释放锁
            distributedLock.unlock(lockKey, requestId);
        }
    }
}

六、生产环境最佳实践

6.1 Key 设计规范

java 复制代码
// ✅ 推荐:使用冒号分隔,语义清晰
String key = "user:info:1001";
String key = "order:2024:5678";
String key = "product:stock:999";

// ❌ 避免:过长或无意义的 key
String key = "u_i_1001";
String key = "this_is_a_very_long_key_name_that_wastes_memory:1001";

规范要点:

  • 使用 业务模块:子模块:id 的层级结构
  • 控制 key 长度在 20-50 字符之间
  • 避免特殊字符和空格
  • 统一命名风格

6.2 过期时间策略

java 复制代码
/**
 * 设置随机过期时间,防止缓存雪崩
 */
public void setWithRandomTtl(String key, Object value, long baseTtlSeconds) {
    // 基础时间 ± 20% 随机波动
    long randomOffset = (long) (baseTtlSeconds * 0.2 * Math.random());
    long ttl = baseTtlSeconds + (Math.random() > 0.5 ? randomOffset : -randomOffset);
    
    redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}

6.3 BigKey 处理

什么是 BigKey?

  • String 类型:value 超过 10KB
  • List/Hash/Set/ZSet:元素数量超过 5000

危害:

  • 占用大量内存
  • 网络拥塞
  • 阻塞 Redis 主线程

解决方案:

java 复制代码
/**
 * 拆分大 Hash
 */
public void splitBigHash(String bigKey, Map<String, Object> data) {
    int shardCount = 10;
    for (Map.Entry<String, Object> entry : data.entrySet()) {
        int shard = Math.abs(entry.getKey().hashCode() % shardCount);
        String shardKey = bigKey + ":" + shard;
        redisTemplate.opsForHash().put(shardKey, entry.getKey(), entry.getValue());
    }
}

/**
 * 分批获取大集合
 */
public Set<Object> scanLargeSet(String key, int count) {
    Cursor<Object> cursor = redisTemplate.opsForSet()
        .scan(key, ScanOptions.scanOptions().count(count).build());
    
    Set<Object> result = new HashSet<>();
    while (cursor.hasNext()) {
        result.add(cursor.next());
    }
    return result;
}

6.4 监控与告警

java 复制代码
@Component
@Slf4j
public class RedisHealthMonitor {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Scheduled(fixedRate = 60000) // 每分钟执行
    public void monitorRedisHealth() {
        try {
            Properties info = redisTemplate.getConnectionFactory()
                .getConnection().info();
            
            // 内存使用
            long usedMemory = Long.parseLong(info.getProperty("used_memory"));
            long maxMemory = Long.parseLong(info.getProperty("maxmemory"));
            double memoryUsage = (double) usedMemory / maxMemory * 100;
            
            // 连接数
            long connectedClients = Long.parseLong(info.getProperty("connected_clients"));
            
            // QPS
            long ops = Long.parseLong(info.getProperty("instantaneous_ops_per_sec"));
            
            // 命中率
            long keyspaceHits = Long.parseLong(info.getProperty("keyspace_hits"));
            long keyspaceMisses = Long.parseLong(info.getProperty("keyspace_misses"));
            double hitRate = (double) keyspaceHits / (keyspaceHits + keyspaceMisses) * 100;
            
            log.info("Redis健康检查 - 内存: {:.2f}%, 连接: {}, QPS: {}, 命中率: {:.2f}%",
                memoryUsage, connectedClients, ops, hitRate);
            
            // 告警
            if (memoryUsage > 80) {
                sendAlert("Redis内存使用率过高: " + String.format("%.2f", memoryUsage) + "%");
            }
            
            if (hitRate < 60) {
                sendAlert("Redis缓存命中率过低: " + String.format("%.2f", hitRate) + "%");
            }
            
        } catch (Exception e) {
            log.error("Redis健康检查失败", e);
        }
    }

    private void sendAlert(String message) {
        // TODO: 发送钉钉/企业微信/邮件告警
        log.warn("告警: {}", message);
    }
}

七、总结与展望

7.1 本文要点回顾

持久化机制 :深入理解 RDB 和 AOF 的原理与配置

主从复制 :掌握读写分离架构的搭建与监控

哨兵模式 :实现高可用的自动故障转移

事务与 Lua :保证复杂操作的原子性

分布式锁 :从基础到高级的完整实现

最佳实践:生产环境的配置与优化建议

7.2 下篇预告

在下一篇文章《Redis 从入门到精通:Spring Boot 实战三部曲(三)------ 高级特性与性能优化》中,我们将探讨:

  • 🔥 Redis Cluster 集群架构深度解析
  • ⚡ 高性能原理与底层数据结构
  • 🎯 性能优化技巧与调优实战
  • 🛡️ 缓存穿透/击穿/雪崩的高级解决方案
  • 📊 大规模 Redis 应用的架构设计

7.3 学习建议

  1. 理论结合实践:不仅要理解原理,更要动手搭建环境
  2. 关注官方文档:Redis 更新频繁,及时了解新特性
  3. 性能测试:使用 redis-benchmark 进行压力测试
  4. 监控先行:生产环境一定要做好监控和告警

📚 参考资料


觉得有用?欢迎点赞、收藏、转发!

下一篇更精彩,敬请期待! 🚀

系列文章:

  • 第一篇 Redis 从入门到精通:Spring Boot 实战三部曲(一)------ 基础核心与快速上手
  • 第二篇 Redis 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶原理与高可用架构
  • 第三篇 Redis 从入门到精通:Spring Boot 实战三部曲(三)------ 高级特性与性能优化
相关推荐
星晨雪海2 小时前
基于 SpringBoot + Redis (Lettuce) + RabbitMQ 实现「Redis 预扣库存 + 异步同步数据库」
数据库·spring boot·java-rabbitmq
lzp07913 小时前
元数据驱动开发 - 面向对象编程思想的补充(上)
spring boot·后端·ui
hai31524754310 小时前
RISC-V核E203核前向旁路的架构性顽疾
驱动开发·架构·硬件架构·硬件工程·risc-v
意图共鸣10 小时前
意图共鸣科技《认知智能白皮书》——感知与执行分离:认知架构(CA)如何重塑大模型底层结构
人工智能·架构
王莎莎-MinerU11 小时前
MinerU 深度技术解析:从架构原理到生产部署的全面指南
css·人工智能·自然语言处理·架构·ocr·个人开发
canonical_entropy11 小时前
Harness Engineering 之外:从非线性动力系统控制理解吸引子引导工程
架构·aigc·ai编程
Jiude11 小时前
AI 写代码太快之后,团队协作反而更难了
人工智能·架构·github
cfm_291411 小时前
Redis数据安全性解析
数据库·redis·缓存