Redis集群与性能优化
Redis集群与性能优化
前置知识
在学习本教程之前,建议您已经掌握:
- Redis 基础知识和核心数据类型
- Redis 高级特性(事务、管道、发布订阅等)
- Spring Boot 与 Redis 的集成
Redis 集群架构
Redis 提供了多种集群架构方案,以满足不同场景下的高可用和扩展性需求。
1. 主从复制(Master-Slave)
主从复制是 Redis 最基本的高可用方案,一个主节点(Master)可以有多个从节点(Slave)。
特点:
- 主节点负责读写操作,从节点只负责读操作
- 主节点数据更新后,自动同步到从节点
- 主节点故障时,需要手动将从节点提升为主节点
适用场景:
- 读多写少的应用
- 数据备份和容灾
2. 哨兵模式(Sentinel)
哨兵模式是在主从复制基础上,增加了自动故障检测和转移功能。
特点:
- 监控主从节点的运行状态
- 当主节点故障时,自动选举新的主节点
- 通知客户端主节点变更信息
适用场景:
- 需要高可用但数据量不大的场景
- 对数据一致性要求较高的场景
3. 集群模式(Cluster)
Redis Cluster 是 Redis 的分布式解决方案,支持数据自动分片和高可用。
特点:
- 数据自动分片,每个节点存储部分数据
- 无中心架构,每个节点都与其他节点直接通信
- 支持节点的动态添加和删除
- 部分节点故障时,集群仍能继续工作
适用场景:
- 数据量大,需要横向扩展的场景
- 需要高可用和高性能的场景
Docker 搭建主从复制
使用 Docker 可以快速搭建 Redis 主从复制环境。
# docker-compose.yml
version: '3'
services:
redis-master:
image: redis:6.2
container_name: redis-master
ports:
- "6379:6379"
volumes:
- ./master/redis.conf:/etc/redis/redis.conf
command: redis-server /etc/redis/redis.conf
networks:
- redis-net
redis-slave-1:
image: redis:6.2
container_name: redis-slave-1
ports:
- "6380:6379"
volumes:
- ./slave1/redis.conf:/etc/redis/redis.conf
command: redis-server /etc/redis/redis.conf
depends_on:
- redis-master
networks:
- redis-net
redis-slave-2:
image: redis:6.2
container_name: redis-slave-2
ports:
- "6381:6379"
volumes:
- ./slave2/redis.conf:/etc/redis/redis.conf
command: redis-server /etc/redis/redis.conf
depends_on:
- redis-master
networks:
- redis-net
networks:
redis-net:
driver: bridge
主节点配置文件 master/redis.conf
:
bind 0.0.0.0
protected-mode yes
port 6379
requirepass master123
从节点配置文件 slave1/redis.conf
和 slave2/redis.conf
:
bind 0.0.0.0
protected-mode yes
port 6379
replicaof redis-master 6379
masterauth master123
requirepass slave123
启动 Docker 容器:
docker-compose up -d
Spring Boot 配置主从复制
在 Spring Boot 中配置 Redis 主从复制,可以使用 Lettuce 或 Jedis 客户端。
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
// 创建主节点配置
RedisStandaloneConfiguration masterConfig = new RedisStandaloneConfiguration();
masterConfig.setHostName("localhost");
masterConfig.setPort(6379);
masterConfig.setPassword(RedisPassword.of("master123"));
// 创建读写分离配置
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // 优先从从节点读取,从节点不可用时从主节点读取
.build();
return new LettuceConnectionFactory(masterConfig, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 设置序列化器
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
Redis 持久化
Redis 提供了两种持久化机制:RDB 和 AOF。
1. RDB 持久化
RDB(Redis Database)是 Redis 默认的持久化方式,它通过创建快照(snapshot)来保存数据库在某个时间点的状态。
配置示例:
# redis.conf
# 900秒内至少有1个key被修改,则触发保存
save 900 1
# 300秒内至少有10个key被修改,则触发保存
save 300 10
# 60秒内至少有10000个key被修改,则触发保存
save 60 10000
# RDB文件名
dbfilename dump.rdb
# RDB文件保存路径
dir /var/lib/redis
# 是否压缩RDB文件
rdbcompression yes
# 保存RDB文件时是否进行校验
rdbchecksum yes
优点:
- 文件紧凑,适合备份和恢复
- 性能影响小,fork子进程进行持久化
- 恢复速度快
缺点:
- 可能会丢失最后一次快照后的数据
- fork子进程时可能会导致服务短暂暂停
2. AOF 持久化
AOF(Append Only File)持久化记录服务器执行的所有写操作命令,并在服务器启动时重新执行这些命令来恢复数据。
配置示例:
# redis.conf
# 开启AOF持久化
appendonly yes
# AOF文件名
appendfilename "appendonly.aof"
# 同步策略:always、everysec、no
# always: 每次写操作都同步到磁盘,最安全但最慢
# everysec: 每秒同步一次,推荐设置
# no: 由操作系统决定何时同步,最快但最不安全
appendfsync everysec
# AOF重写触发条件
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
优点:
- 数据安全性高,支持不同的同步策略
- 可以在后台对AOF文件进行重写,不影响服务
缺点:
- 文件体积大,恢复速度慢
- 写入性能略低于RDB
3. 混合持久化
Redis 4.0 引入了混合持久化,结合了 RDB 和 AOF 的优点。
配置示例:
# redis.conf
# 开启AOF持久化
appendonly yes
# 开启混合持久化
aof-use-rdb-preamble yes
优点:
- 结合了RDB的快速恢复和AOF的数据安全性
- 文件体积小于纯AOF
连接池配置
合理配置 Redis 连接池可以提高性能和稳定性。
RedisPoolConfig
@Configuration
public class RedisPoolConfig {
@Bean
public LettuceConnectionFactory lettuceConnectionFactory(
RedisStandaloneConfiguration redisConfig,
LettucePoolingClientConfiguration poolConfig) {
return new LettuceConnectionFactory(redisConfig, poolConfig);
}
@Bean
public RedisStandaloneConfiguration redisStandaloneConfiguration() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("localhost");
config.setPort(6379);
config.setPassword(RedisPassword.of("password"));
config.setDatabase(0);
return config;
}
@Bean
public LettucePoolingClientConfiguration lettucePoolConfig() {
// 连接池配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(100); // 最大连接数
poolConfig.setMaxIdle(20); // 最大空闲连接数
poolConfig.setMinIdle(5); // 最小空闲连接数
poolConfig.setMaxWaitMillis(2000); // 获取连接最大等待时间
poolConfig.setTestOnBorrow(true); // 获取连接时检测连接是否有效
poolConfig.setTestWhileIdle(true); // 空闲时检测连接是否有效
poolConfig.setTimeBetweenEvictionRunsMillis(30000); // 空闲连接检测周期
return LettucePoolingClientConfiguration.builder()
.poolConfig(poolConfig)
.commandTimeout(Duration.ofMillis(5000)) // 命令执行超时时间
.shutdownTimeout(Duration.ofMillis(2000)) // 关闭超时时间
.build();
}
}
缓存优化
1. 缓存预热
缓存预热是指在系统启动时,提前将数据加载到缓存中,避免用户请求时因缓存未命中而导致的性能问题。
@Component
@Slf4j
public class CacheWarmUpRunner implements ApplicationRunner {
@Autowired
private ProductService productService;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("开始缓存预热...");
long startTime = System.currentTimeMillis();
// 加载热门商品到缓存
List<Product> hotProducts = productService.findHotProducts();
for (Product product : hotProducts) {
productService.cacheProduct(product);
}
// 加载系统配置到缓存
productService.loadSystemConfig();
long endTime = System.currentTimeMillis();
log.info("缓存预热完成,耗时: {} ms", (endTime - startTime));
}
}
2. 缓存穿透解决方案
缓存穿透是指查询一个不存在的数据,导致请求直接落到数据库上。
CacheOptimizeDemo
@Service
@Slf4j
public class CacheOptimizeDemo {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ProductMapper productMapper;
private static final String EMPTY_CACHE = "EMPTY_CACHE";
private static final long EMPTY_CACHE_EXPIRE = 60; // 60秒
/**
* 使用空值缓存解决缓存穿透
*/
public Product getProductById(Long id) {
String key = "product:" + id;
// 查询缓存
String productJson = redisTemplate.opsForValue().get(key);
// 缓存命中
if (productJson != null) {
// 判断是否为空值缓存
if (EMPTY_CACHE.equals(productJson)) {
log.info("命中空值缓存, id: {}", id);
return null;
}
// 反序列化并返回
log.info("缓存命中, id: {}", id);
return JSON.parseObject(productJson, Product.class);
}
// 缓存未命中,查询数据库
log.info("缓存未命中, 查询数据库, id: {}", id);
Product product = productMapper.selectById(id);
// 数据库中不存在该商品,缓存空值
if (product == null) {
log.info("数据库中不存在该商品, 缓存空值, id: {}", id);
redisTemplate.opsForValue().set(key, EMPTY_CACHE, EMPTY_CACHE_EXPIRE, TimeUnit.SECONDS);
return null;
}
// 数据库中存在,缓存商品信息
log.info("数据库中存在该商品, 缓存商品信息, id: {}", id);
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 5, TimeUnit.MINUTES);
return product;
}
/**
* 使用布隆过滤器解决缓存穿透
*/
public Product getProductWithBloomFilter(Long id) {
String key = "product:" + id;
// 使用布隆过滤器判断商品ID是否存在
if (!bloomFilterContains(id)) {
log.info("布隆过滤器拦截, 商品不存在, id: {}", id);
return null;
}
// 查询缓存
String productJson = redisTemplate.opsForValue().get(key);
// 缓存命中
if (productJson != null) {
log.info("缓存命中, id: {}", id);
return JSON.parseObject(productJson, Product.class);
}
// 缓存未命中,查询数据库
log.info("缓存未命中, 查询数据库, id: {}", id);
Product product = productMapper.selectById(id);
// 数据库中存在,缓存商品信息
if (product != null) {
log.info("数据库中存在该商品, 缓存商品信息, id: {}", id);
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 5, TimeUnit.MINUTES);
}
return product;
}
/**
* 模拟布隆过滤器判断元素是否存在
*/
private boolean bloomFilterContains(Long id) {
// 实际项目中可以使用 Redisson 或 Guava 的布隆过滤器实现
// 这里简化处理,假设 id > 0 且 id < 10000 的商品存在
return id > 0 && id < 10000;
}
}
3. 缓存雪崩解决方案
缓存雪崩是指在同一时间大量缓存失效,导致请求直接落到数据库上。
CacheAvalancheDemo
@Service
@Slf4j
public class CacheAvalancheDemo {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ProductMapper productMapper;
/**
* 使用随机过期时间解决缓存雪崩
*/
public Product getProductWithRandomExpire(Long id) {
String key = "product:" + id;
// 查询缓存
String productJson = redisTemplate.opsForValue().get(key);
// 缓存命中
if (productJson != null) {
log.info("缓存命中, id: {}", id);
return JSON.parseObject(productJson, Product.class);
}
// 缓存未命中,查询数据库
log.info("缓存未命中, 查询数据库, id: {}", id);
Product product = productMapper.selectById(id);
if (product != null) {
// 设置随机过期时间,避免同时过期
int expireTime = 300 + new Random().nextInt(60); // 300~359秒
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), expireTime, TimeUnit.SECONDS);
log.info("缓存商品信息, 过期时间: {}秒, id: {}", expireTime, id);
}
return product;
}
/**
* 使用互斥锁解决缓存击穿
*/
public Product getProductWithMutex(Long id) throws InterruptedException {
String key = "product:" + id;
String lockKey = "lock:product:" + id;
// 查询缓存
String productJson = redisTemplate.opsForValue().get(key);
// 缓存命中
if (productJson != null) {
log.info("缓存命中, id: {}", id);
return JSON.parseObject(productJson, Product.class);
}
// 获取互斥锁
boolean locked = tryLock(lockKey);
if (!locked) {
// 获取锁失败,等待一段时间后重试
Thread.sleep(50);
return getProductWithMutex(id);
}
try {
// 双重检查,再次查询缓存
productJson = redisTemplate.opsForValue().get(key);
if (productJson != null) {
log.info("缓存命中(双重检查), id: {}", id);
return JSON.parseObject(productJson, Product.class);
}
// 查询数据库
log.info("缓存未命中, 查询数据库, id: {}", id);
Product product = productMapper.selectById(id);
if (product != null) {
// 设置缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 5, TimeUnit.MINUTES);
log.info("缓存商品信息, id: {}", id);
}
return product;
} finally {
// 释放锁
unlock(lockKey);
}
}
/**
* 尝试获取锁
*/
private boolean tryLock(String key) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放锁
*/
private void unlock(String key) {
redisTemplate.delete(key);
}
}
监控与告警
1. 关键指标
监控 Redis 的关键指标:
- 内存使用情况:used_memory, used_memory_rss
- 命令执行情况:total_commands_processed, instantaneous_ops_per_sec
- 网络情况:connected_clients, rejected_connections
- 持久化情况:rdb_last_save_time, aof_current_size
- 主从复制情况:master_link_status, connected_slaves
2. Prometheus 监控
使用 Prometheus 和 Redis Exporter 监控 Redis。
Docker Compose 配置
# docker-compose.yml
version: '3'
services:
redis:
image: redis:6.2
container_name: redis
ports:
- "6379:6379"
networks:
- monitoring
redis-exporter:
image: oliver006/redis_exporter
container_name: redis-exporter
ports:
- "9121:9121"
environment:
- REDIS_ADDR=redis://redis:6379
networks:
- monitoring
depends_on:
- redis
prometheus:
image: prom/prometheus
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- monitoring
depends_on:
- redis-exporter
grafana:
image: grafana/grafana
container_name: grafana
ports:
- "3000:3000"
networks:
- monitoring
depends_on:
- prometheus
networks:
monitoring:
driver: bridge
Prometheus 配置文件 prometheus.yml
:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']
3. Grafana 面板
在 Grafana 中导入 Redis Dashboard(ID: 763),可以查看 Redis 的各项指标。
Grafana Dashboard 配置
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": 763,
"graphTooltip": 0,
"id": 1,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"hiddenSeries": false,
"id": 1,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.3.7",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "redis_memory_used_bytes",
"interval": "",
"legendFormat": "Memory Used",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Memory Usage",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "bytes",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"refresh": "5s",
"schemaVersion": 26,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Redis Dashboard",
"uid": "redis",
"version": 1
}
最佳实践
1. 集群选择
建议
- 单机模式:适用于开发测试环境或数据量小的场景
- 主从复制:适用于读多写少的场景
- 哨兵模式:适用于需要高可用但数据量不大的场景
- 集群模式:适用于数据量大,需要横向扩展的场景
2. 持久化策略
注意事项
- 如果对数据安全性要求高,建议使用 AOF 持久化或混合持久化
- 如果对性能要求高,可以使用 RDB 持久化
- 根据业务场景调整持久化频率,避免过于频繁的持久化影响性能
3. 内存优化
建议
- 设置合理的 maxmemory 和淘汰策略
- 使用 String 类型时,考虑使用整数编码来节省内存
- 使用 Hash 类型存储对象,可以节省内存
- 定期清理过期的键
4. 命令优化
注意事项
- 避免使用 KEYS 命令,使用 SCAN 命令代替
- 避免一次获取大量数据,使用分页获取
- 使用批量命令(MGET、MSET)代替多次单个命令
- 使用 Pipeline 减少网络往返时间
总结
本文详细介绍了 Redis 的集群配置、持久化机制和性能优化:
- ✅ 集群架构:主从复制、哨兵模式、集群模式
- ✅ 持久化机制:RDB、AOF、混合持久化
- ✅ 连接池配置:最大连接数、空闲连接、超时时间等
- ✅ 缓存优化:缓存预热、缓存穿透、缓存雪崩解决方案
- ✅ 监控与告警:关键指标、Prometheus、Grafana
学习建议
- 深入学习 Redis 的内部原理
- 了解更多 Redis 在实际项目中的应用场景
- 掌握 Redis 的性能调优技巧
希望这篇文章能帮助您更好地使用 Redis 集群和优化 Redis 性能!如果您有任何问题,欢迎在评论区讨论。