- Redis
- 环境安装
- docker run -d –name redis -p 6379:6379 redis
- docker exec -it redis redis-cli (进入容器安装)
- 安装 redis-cli
- brew install redis
- redis-cli –version
- 连接 redis
- redis-cli -h 127.0.0.1 -p 6379
- ping
- PONG
- Redis 定位
- 性能:减少 DB 压力
- 并发:原子操作
- 临时数据:状态、计数、锁
- 不适合场景
- 强一致性核心数据
- 超大对象
- 长期冷数据
- 基本特性
- 命令执行是单线程
- 网络 I/O 是多路复用
- 核心数据结构
- 清空数据
- FLUSHDB
- 查看有多少 key
- DBSIZE
- String
- 基本 GET/SET
- 带过期时间 TTL
- EX 是秒,PX 是毫秒
- TTL 返回剩余的秒数
- TTL = -1: 存在但没过期时间,-2 不存在
- 原子自增(计数器)
- 基本 GET/SET
- List
- 左进右出:队列 FIFO
- 阻塞队列
- BRPOP key timeout
- key 要监听的队列
- timeout 最大等待时间(s) 0=无限等待,5=最多等5s
- 左进右出:队列 FIFO
- Hash (存对象)
- 写入写出
- 写入写出
- Set (去重,关系,共同好友)
- 去重集合
- 交集、并集、差集
- 去重集合
- ZSet (排行榜:积分、热度、TopN)
- ZSet = Set + Score
- 添加与排序读取
- 增加分数与查询名次
- TOPN
- 按成员删除
- 按 score 范围删除
- 按排名删除
- Bitmap (用户签到/布尔状态)
- 场景:统计 2026-02 月的签到情况
- 思路
- 一个用户 = 1 个 Bitmap
- 一天 = 1 bit
- 签到 = 1
- 未签到 = 0
- key = sign:用户 ID:202602
- offset = 第几天 - 1
- 基础实验
- 第一天签到 + 第二天签到
- 查询某天是否签到
- 统计本月签到天数
- 第一天签到 + 第二天签到
- HyperLogLog (UV/去重统计)
- 场景:统计某天网站 UV
- 模拟用户访问
- 统计 UV
- 特点
- 内存:固定 12KB
- 准确度:99%
- Geo (地理位置/附近的人)
- 场景
- 查找 5km 内的商家
- ZSet + GeoHash
- 添加地理位置
- 查看两点距离
- 查询附近 1500km 的城市
- 带距离排序
- 场景
- 通用排查命令
- 查看 Key
- 一般生产环境中不建议用 KEYS,建议使用 SCAN
- 查看类型/是否存在
- 删除 key
- 查看 Key
- 清空数据
- 缓存系统实战
- 目标
- 实现用户查询接口: Get /user/{id}
- 架构
- Controller -> Service -> Redis -> FakeDB
- 逻辑
- 先查 Redis
- 没命中查数据库
- 写回 Redis
- 加 TTL
- 防穿透 + 防击穿
- Java 实现
- 初始化项目 https://start.spring.io
- 配置 RedisTemplate
- 编写 Service
- 编写 Controller
- 访问 localhost:8080/user/1
- 查看 redis 内容
- 压测
- ab -n 10000 -c 100 http://localhost:8080/user/1
- 初始化项目 https://start.spring.io
- 目标
- 秒杀系统实战
- 项目结构
- 库存扣减
- 消息队列
- 分布式锁
- 限流
- service
- controller
- 压测测试
- ab -n 1000 -c 200 http://localhost:8080/seckill/1
- 进阶
- 滑动窗口限流
- lua
- service
- lua
- 令牌桶
- lua
- lua
- 滑动窗口限流
- 项目结构
- 持久化
- RDB(快照)
- 机制:周期性生成内存快照文件 dump.rdb
- 优点:恢复快、文件紧凑
- 缺点:可能丢失最近一次快照之后的数据
- 关键点:fork 触发 COW,会带来短暂的延迟和额外内存占用
- AOF(追加日志)
- 机制:写命令追加到 appendonly.aof
- 刷盘策略
- appendfsync always:每条都写 fsync,最安全最慢
- everysec:每秒 fsync,最常用,最多丢 1s
- no:交给 OS,风险高
- AOF 重写:压缩日志,避免无限膨胀
- 混合持久化
- AOF 前半段是 RDB 快照,后半段是增量 AOF
- 实验一:只开 RDB,模拟宕机恢复
- 只启用 RDB 快照
- 编写配置文件 redis-rdb.conf
- 启动 redis 容器
- mkdir -p ./redis-data
- docker rm -f redis-rdb 2>/dev/null
- 验证 docker logs redis-rdb –tail 20
- 进入容器
- docker exec -it redis-rdb redis-cli
- 写入大量数据
- 观察 persistence
- 手动触发 BGSAVE
- 制造快照后的新增数据(丢失窗口)
- 强制宕机
- docker kill -s KILL redis-rdb
- 重启 redis
- docker start redis-rdb
- 验证 rdb 文件
- 实验 2:只开 AOF,验证刷盘策略差异
- 清理旧容器和目录
- docker rm -f redis-aof 2>/dev/null
- rm -rf ./redis-aof-data
- 准备三份配置
- 清空数据&启动 redis
- 检查 AOF 开启
- 写入负载 + 强制关闭
- docker kill -s KILL redis-aof
- 重启 Redis
- docker start redis-aof
- 查看计数
- 清理旧容器和目录
- 实验 3:AOF rewrite 触发与观测
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
- RDB(快照)
- 主从复制
- 目的:读扩展、数据冗余、高可用
- 全量复制:第一次同步或者 backlog 不够时
- 增量复制(PSYNC):短线重连后从 backlog 补差异
- 实验 1:搭主从并验证数据一致
- 启动 Master
- 启动 Replica
- 创建并加入网络
- docker network create redis-net 2>/dev/null
- docker network connect redis-net redis-master
- 启动 replica 并加入网络
- 查看复制状态
- 验证数据一致性
- 主库写入 + 从库读取
- 验证从库默认只读
- 查看复制延迟
- 排障 INFO replication
- 创建并加入网络
- 启动 Master
- 哨兵 Sentinel (高可用)
- 目的:监控、选主、故障转移、通知客户端
- 核心:主观下线、客观下线、选举与 failover
- 客户端连接地址:不用写死 master 地址,而是通过哨兵获取当前 master
- 实验:搭建 1 主 2 从+ 3 Sentinel
- 准备目录
- mkdir -p redis-sentinel-lab/{conf,data}
- 准备 Redis 配置
- master 配置
- replica 配置
- sentinel 配置
- quorum 2:3 个 sentinel 里至少 2 个认为 master 有问题,才会进入客观下线/转移流程
- down-after-milliseconds:5s 没有响应就 SDOWN。
- docker-compose 启动
- 验证主从复制
- redis-cli -h 127.0.0.1 -p 6379 -a 123456 INFO replication
- redis-cli -h 127.0.0.1 -p 6380 -a 123456 INFO replication
- redis-cli -h 127.0.0.1 -p 6381 -a 123456 INFO replication
- 验证写主从一致性
- 验证 Sentinel 监控是否生效
- 查看主库信息 redis-cli -p 26379 SENTINEL master mymaster
- 查看从库信息 redis-cli -p 26379 SENTINEL slaves mymaster
- master 配置
- 准备目录
- 实验:故障演练
- 写入测试数据,验证切主后数据仍然可写
- 观察主从拓扑
- 查看 Sentinel 实时日志
- docker compose logs -f –tail 200 sentinel-1
- 模拟主库宕机
- docker kill -s KILL $(docker compose ps -q redis-master)
- 等待故障转移完成(5-15s)
- 确定新 master
- 验证其中一个哪个变成 master
- 验证切主后仍然可写
- 启动旧 master
- docker compose up -d redis-master
- 旧 master 变成 slave
- 最终一致性验证
- 写入测试数据,验证切主后数据仍然可写
- 实验:SpringBoot 无感切主
- 验证 Sentiel 当前 master
- 配置 Sentinel 连接
- 编写 Controller 验证读写
- 构建镜像
- docker build -t redis-demo .
- 切主前写入
- 模拟主库宕机
- docker kill -s KILL $(docker compose ps -q redis-master)
- 观察 sentinel 选主
- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
- 验证 Sentiel 当前 master
- 内存管理
- INFO memory
- used_memory: Redis 实际分配的内存
- used_memory_human:人类可读的内粗
- used_memory_rss:进程在 OS 视角占用的物理内存
- used_memory_peak:历史峰值
- mem_fragmentation:碎片比 = rss/used_memory >= 2 碎片偏高 - 上限相关
- maxmemory:代表你给 Redis 的硬上限,等于 0 代表未设置内存上限,Redis 会继续申请内存知道吃光资源,不会触发淘汰侧路
- maxmemory-policy:代表 Redis 达到上限时怎么处理
- 淘汰策略
- allkeys-lfu (强烈推荐,缓存业务首选)
- 所有 key 都可能被淘汰
- 适合热点明显的缓存
- allkeys-lru
- 适合访问模式更均匀,有最近性的场景
- volatile-ttl
- 只淘汰带过期时间的 key
- noeviction
- 拒绝写入,适合强一致性
- allkeys-lfu (强烈推荐,缓存业务首选)
- 实验:设置 maxmemory + 不同淘汰策略对比
- 设置较小的 maxmemory
- redis-cli -a 123456 CONFIG SET maxmemory 10mb
- 测试策略
- redis-cli -a 123456 FLUSHDB
- redis-cli -a 123456 CONFIG SET maxmemory-policy noeviction
- 持续写入大 value
- 观察指标
- redis-cli -a 123456 INFO stats | egrep “evicted_keys|keyspace_hits|keyspace_misses”
- redis-cli -a 123456 INFO memory | egrep “used_memory_human|maxmemory_human|mem_fragmentation_ratio”
- 设置较小的 maxmemory
- 实验:LFU vs LRU 对热点的保护效果
- 目标:构造一个热点 key 与大量冷 key,验证 LFU 更能保护热点
- 策略设置
- redis-cli -a 123456 CONFIG SET maxmemory-policy allkeys-lfu
- 写入热点 key
- redis-cli -a 123456 SET hot “1”
- 增加访问频率
- for i in $(seq 1 20000); do redis-cli -a 123456 GET hot > /dev/null; done
- 写入大量冷 key
- 检查 hot 是否存在
- 实验:内存碎片 fragmentation 观察与整理
- 制造碎片
- 反复创建/删除很多不同大小的 value
- Hash/List 不断增长、缩小
- 缓解手段
- 避免频繁大幅变更 value 大小
- 拆分 bigkey
- 合理设置 maxmemory,预留空间
- Redis 4+ 可以考虑 memory purge
- 制造碎片
- 实验:BigKey 检测与治理
- 构造 bigkey
- 查看内存
- 粗定位 –bigkeys (抽样/扫描)
- 看到按类型统计的最大 key
- 会扫描 keyspace,生产慎用
- 精确测量 MEMORY USAGE
- redis-cli -a 123456 MEMORY USAGE big:hash
- 观察 BigKey 对延迟的影响
- 打开延迟监控
- redis-cli -a 123456 CONFIG SET latency-monitor-threshold 10
- 删除 bigkey
- 查看延迟事件
- redis-cli -a 123456 LATENCY LATEST
- 打开延迟监控
- 危害
- 阻塞主线程
- 复制/持久化压力
- 网络抖动
- 治理方案
- 拆分设计
- 构造 bigkey
- INFO memory
- 环境安装
附录
缓存系统实战
缓存系统实战 application.properties:
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.timeout=3000
RedisTemplate 配置类:
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
@Bean
public ObjectMapper objectMapper() {
return JsonMapper.builder().build();
}
}
Service 类:
@Service
@RequiredArgsConstructor
public class UserService {
private final StringRedisTemplate redis;
private final ObjectMapper mapper;
private final FakeDatabase db;
public User getUser(Long id) throws Exception {
String key = "user:" + id;
// 查缓存
String cache = redis.opsForValue().get(key);
if (cache != null) {
if ("NULL".equals(cache)) {
return null;
}
return mapper.readValue(cache, User.class);
}
// 防缓存击穿(热点 key 失效瞬间,大量请求直接打数据库),加锁
Boolean lock = redis.opsForValue()
.setIfAbsent("lock:" + id, "1", Duration.ofSeconds(5));
if (Boolean.FALSE.equals(lock)) {
Thread.sleep(50);
return getUser(id);
}
// 查数据库
User user = db.queryUser(id);
if (user == null) {
// 防缓存穿透,请求的数据本身就不存在,攻击者疯狂请求,每次请求都打 DB
redis.opsForValue().set(key, "NULL", Duration.ofSeconds(30));
return null;
}
// 写缓存 + 随机 TTL,防止缓存雪崩
int ttl = 60 + new Random().nextInt(20);
String json = mapper.writeValueAsString(user);
redis.opsForValue().set(key, json, Duration.ofSeconds(ttl));
return user;
}
}
Controller 类
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/user/{id}")
public User get(@PathVariable Long id) throws Exception {
return userService.getUser(id);
}
}
FakeDB 类
@Component
public class FakeDatabase {
public User queryUser(Long id) {
try {
Thread.sleep(100);
} catch (Exception ignored) {
}
if (id == 999L) {
return null;
}
return new User(id, "User_" + id);
}
}
秒杀系统实战
SeckillController
@RestController
@RequiredArgsConstructor
public class SeckillController {
private final SeckillService service;
@PostMapping("/seckill/{userId}")
public String seckill(@PathVariable Long userId) {
int r = service.seckill(userId);
return switch (r) {
case 1 -> "成功";
case 0 -> "库存不足";
case -1 -> "重复下单";
case -2 -> "限流";
case -3 -> "重复请求";
default -> "失败";
};
}
}
RateLimiter
@Component
@RequiredArgsConstructor
public class RateLimiter {
private final StringRedisTemplate redis;
public boolean allow(String key, int limit) {
String redisKey = "limit:" + key + ":" + Instant.now().getEpochSecond();
Long count = redis.opsForValue().increment(redisKey);
if (count == 1) {
redis.expire(redisKey, Duration.ofSeconds(2));
}
return count <= limit;
}
}
RedisLock
@Component
@RequiredArgsConstructor
public class RedisLock {
private final StringRedisTemplate redis;
private static final String LOCK_PREFIX = "lock:";
private static final long DEFAULT_EXPIRE = 5;
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setScriptText(
"""
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
);
UNLOCK_SCRIPT.setResultType(Long.class);
}
public String tryLock(String key) {
String lockKey = LOCK_PREFIX + key;
// 每个线程唯一 id
String lockValue = UUID.randomUUID().toString();
Boolean success = redis.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(DEFAULT_EXPIRE));
if (Boolean.TRUE.equals(success)) {
return lockValue;
}
return null;
}
public boolean unlock(String key, String lockValue) {
String lockKey = LOCK_PREFIX + key;
Long result = redis.execute(
UNLOCK_SCRIPT,
Collections.singletonList(lockKey),
lockValue
);
return Long.valueOf(1).equals(result);
}
}
LuaConfig
@Configuration
public class LuaConfig {
@Bean
public DefaultRedisScript<Long> stockScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/stock.lua"));
script.setResultType(Long.class);
return script;
}
}
stock.lua
-- KEYS[1] = 库存 key
-- KEYS[2] = 订单集合 key
-- ARGV[1] = 用户 id
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
-- 库存不足
return 0
end
-- 防止重复下单
if redis.call('sismember', KEYS[2], ARGV[1]) == 1 then
-- 重复下单
return -1
end
-- 扣库存
redis.call('decr', KEYS[1])
-- 记录用户下单
redis.call('sadd', KEYS[2], ARGV[1])
-- 成功下单
return 1
OrderQueue
@Component
@RequiredArgsConstructor
public class OrderQueue {
private final StringRedisTemplate redis;
private final ObjectMapper objectMapper;
private static final String QUEUE_KEY = "queue:orders";
public void push(Order order) {
try {
String json = objectMapper.writeValueAsString(order);
redis.opsForList().leftPush(QUEUE_KEY, json);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Order pop() {
String result = redis.opsForList().rightPop(QUEUE_KEY, Duration.ofSeconds(0));
if (result == null) {
return null;
}
try {
return objectMapper.readValue(result, Order.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
OrderWorker
@Component
@RequiredArgsConstructor
public class OrderWorker {
private final OrderQueue queue;
@PostConstruct
public void start() {
Thread work = new Thread(() -> {
while(true) {
Order order =queue.pop();
if (order != null) {
process(order);
}
}
});
work.setDaemon(true);
work.start();
}
private void process(Order order) {
System.out.println("处理订单: " + order);
try {
Thread.sleep(100);
} catch (Exception ignored) {
}
}
}
SeckillService
@Service
@RequiredArgsConstructor
public class SeckillService {
private final StringRedisTemplate redis;
private final DefaultRedisScript<Long> stockScript;
private final OrderQueue queue;
private final RateLimiter rateLimiter;
private final RedisLock redisLock;
public int seckill(Long userId) {
if (!rateLimiter.allow("seckill", 100)) {
return -2; // 系统繁忙
}
// 获取分布式锁
String token = redisLock.tryLock("user:" + userId);
if (token == null) {
return -3;
}
try {
List<String> keys = List.of(
"seckill:stock",
"seckill:orders"
);
Long result = redis.execute(stockScript, keys, userId.toString());
if (result == 1) {
Order order = new Order(userId, System.currentTimeMillis());
queue.push(order);
}
return result.intValue();
} finally {
redisLock.unlock("user:" + userId, token);
}
}
}
秒杀系统滑动窗口限流进阶
ratelimit.lua
-- KEYS[1] = rate limit key (zset)
-- ARGV[1] = now(ms)
-- ARGV[2] = window(ms)
-- ARGV[3] = limit(max requests in window)
-- ARGV[4] = member(unique id for this requests)
-- ARGV[5] = expireSeconds(Key ttl)
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local member = ARGV[4]
local expireSeconds = tonumber(ARGV[5])
-- 1 remove out-of-window
redis.call('zremrangebyscore', key, 0, now - window)
-- 2 current call
local cnt = redis.call('zcard', key)
-- 3 if exceed, reject
if cnt >= limit then
return 0
end
-- 4 add current request
redis.call('zadd', key, now, member)
-- 5 set ttl to avoid key leak
redis.call('expire', key, expireSeconds)
return 1
LuaConfig
@Bean
public DefaultRedisScript<Long> rateLimitScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/ratelimit.lua"));
script.setResultType(Long.class);
return script;
}
SlidingWindowLimit
@Component
@RequiredArgsConstructor
public class SlidingWindowRateLimiter {
private final StringRedisTemplate redis;
private final DefaultRedisScript<Long> rateLimitScript;
public boolean allow(String key, int limit, long windowMs) {
long now = System.currentTimeMillis();
// 每次请求唯一 member
String member = now + "-" + UUID.randomUUID();
// key TTL,一般设置为 window 的 2-3 倍,保证窗口内数据可用并自动回收
long expireSeconds = Math.max(2, (windowMs * 3) / 1000);
String redisKey = "rl:" + key;
Long result = redis.execute(
rateLimitScript,
Collections.singletonList(redisKey),
String.valueOf(now),
String.valueOf(windowMs),
String.valueOf(limit),
member,
String.valueOf(expireSeconds)
);
System.out.println("result:" + result);
return Long.valueOf(1).equals(result);
}
}
秒杀系统进阶令牌桶
token_bucket.lua
-- KEYS[1] = bucket key
-- ARGV[1] = now_ms
-- ARGV[2] = capacity (max tokens)
-- ARGV[3] = refill_rate_per_sec (tokens per second)
-- ARGV[4] = cost (tokens per request, usually 1)
-- ARGV[5] = ttl_seconds (expire key to avoid leaks)
local key = KEYS[1]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local rate = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local ttl = tonumber(ARGV[5])
-- Read current state
local tokens = redis.call('hget', key, 'tokens')
local ts = redis.call('hget', key, 'ts')
if tokens == false or ts == false then
tokens = capacity
ts = now
else
tokens = tonumber(tokens)
ts = tonumber(ts)
end
-- Refill tokens based on elapsed time
local delta_ms = now - ts
if delta_ms < 0 then
delta_ms = 0
end
local refill = (delta_ms / 1000.0) * rate
tokens = math.min(capacity, tokens + refill)
-- Decide allow or reject
if tokens < cost then
redis.call('hset', key, 'tokens', tokens)
redis.call('hset', key, 'ts', now)
redis.call('expire', key, ttl)
return 0
end
-- Consume tokens
tokens = tokens - cost
redis.call('hset', key, 'tokens', tokens)
redis.call('hset', key, 'ts', now)
redis.call('expire', key, ttl)
return 1
TokenBucketRateLimiter
@Component
@RequiredArgsConstructor
public class TokenBucketRateLimiter {
private final StringRedisTemplate redis;
private final DefaultRedisScript<Long> tokenBucketScript;
/**
* 令牌桶限流
* @param key 限流维度
* @param capacity 桶容量
* @param ratePerSec 补充速率
* @param cost 每次请求消耗令牌数
*/
public boolean allow(String key, long capacity, double ratePerSec, long cost) {
long now = System.currentTimeMillis();
String redisKey = "tb:" + key;
// TTL: 建议 >= 桶完全补满的时间 * 2 (避免闲置 key 常驻)
// refill time ≈ capacity / ratePerSec
long refillSeconds = (long) Math.ceil(capacity / Math.max(ratePerSec, 0.0000001));
long ttlSeconds = Math.max(2, refillSeconds * 2);
Long result = redis.execute(
tokenBucketScript,
Collections.singletonList(redisKey),
String.valueOf(now),
String.valueOf(capacity),
String.valueOf(ratePerSec),
String.valueOf(cost),
String.valueOf(ttlSeconds)
);
return Long.valueOf(1).equals(result);
}
}
LuaConfig
@Bean
public DefaultRedisScript<Long> tokenBucketScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("lua/token_bucket.lua"));
script.setResultType(Long.class);
return script;
}
哨兵实验
docker-compose
services:
redis-master:
image: redis:7
container_name: redis-master
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
- ./conf/redis-master.conf:/usr/local/etc/redis/redis.conf
- ./data/master:/data
ports:
- "6379:6379"
networks: [redisnet]
redis-replica-1:
image: redis:7
container_name: redis-replica-1
depends_on: [redis-master]
command: ["redis-server", "/usr/local/etc/redis/redis.conf", "--replicaof", "redis-master", "6379"]
volumes:
- ./conf/redis-replica.conf:/usr/local/etc/redis/redis.conf
- ./data/replica1:/data
ports:
- "6380:6379"
networks: [redisnet]
redis-replica-2:
image: redis:7
container_name: redis-replica-2
depends_on: [redis-master]
command: ["redis-server", "/usr/local/etc/redis/redis.conf", "--replicaof", "redis-master", "6379"]
volumes:
- ./conf/redis-replica.conf:/usr/local/etc/redis/redis.conf
- ./data/replica2:/data
ports:
- "6381:6379"
networks: [redisnet]
sentinel-1:
image: redis:7
container_name: sentinel-1
depends_on: [redis-master, redis-replica-1, redis-replica-2]
command:
[
"sh","-c",
"cp /usr/local/etc/redis/sentinel.conf /data/sentinel.conf && exec redis-sentinel /data/sentinel.conf"
]
volumes:
- ./conf/sentinel.conf:/usr/local/etc/redis/sentinel.conf
- ./data/s1:/data
ports:
- "26379:26379"
networks: [redisnet]
sentinel-2:
image: redis:7
container_name: sentinel-2
depends_on: [redis-master, redis-replica-1, redis-replica-2]
command:
[
"sh","-c",
"cp /usr/local/etc/redis/sentinel.conf /data/sentinel.conf && exec redis-sentinel /data/sentinel.conf"
]
volumes:
- ./conf/sentinel.conf:/usr/local/etc/redis/sentinel.conf
- ./data/s2:/data
ports:
- "26380:26379"
networks: [redisnet]
sentinel-3:
image: redis:7
container_name: sentinel-3
depends_on: [redis-master, redis-replica-1, redis-replica-2]
command:
[
"sh","-c",
"cp /usr/local/etc/redis/sentinel.conf /data/sentinel.conf && exec redis-sentinel /data/sentinel.conf"
]
volumes:
- ./conf/sentinel.conf:/usr/local/etc/redis/sentinel.conf
- ./data/s3:/data
ports:
- "26381:26379"
networks: [redisnet]
networks:
redisnet:
driver: bridge
哨兵故障演练实验
docker-compose.yml
services:
redis-master:
image: redis:7
container_name: redis-master
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
- ./conf/redis-master.conf:/usr/local/etc/redis/redis.conf
- ./data/master:/data
ports:
- "6379:6379"
networks:
redisnet:
ipv4_address: 172.28.0.10
redis-replica-1:
image: redis:7
container_name: redis-replica-1
depends_on: [redis-master]
command: ["redis-server", "/usr/local/etc/redis/redis.conf", "--replicaof", "172.28.0.10", "6379"]
volumes:
- ./conf/redis-replica.conf:/usr/local/etc/redis/redis.conf
- ./data/replica1:/data
ports:
- "6380:6379"
networks: [redisnet]
redis-replica-2:
image: redis:7
container_name: redis-replica-2
depends_on: [redis-master]
command: ["redis-server", "/usr/local/etc/redis/redis.conf", "--replicaof", "172.28.0.10", "6379"]
volumes:
- ./conf/redis-replica.conf:/usr/local/etc/redis/redis.conf
- ./data/replica2:/data
ports:
- "6381:6379"
networks: [redisnet]
sentinel-1:
image: redis:7
container_name: sentinel-1
depends_on: [redis-master, redis-replica-1, redis-replica-2]
command:
[
"sh","-c",
"cp /usr/local/etc/redis/sentinel.conf /data/sentinel.conf && exec redis-sentinel /data/sentinel.conf"
]
volumes:
- ./conf/sentinel.conf:/usr/local/etc/redis/sentinel.conf
- ./data/s1:/data
ports:
- "26379:26379"
networks: [redisnet]
sentinel-2:
image: redis:7
container_name: sentinel-2
depends_on: [redis-master, redis-replica-1, redis-replica-2]
command:
[
"sh","-c",
"cp /usr/local/etc/redis/sentinel.conf /data/sentinel.conf && exec redis-sentinel /data/sentinel.conf"
]
volumes:
- ./conf/sentinel.conf:/usr/local/etc/redis/sentinel.conf
- ./data/s2:/data
ports:
- "26380:26379"
networks: [redisnet]
sentinel-3:
image: redis:7
container_name: sentinel-3
depends_on: [redis-master, redis-replica-1, redis-replica-2]
command:
[
"sh","-c",
"cp /usr/local/etc/redis/sentinel.conf /data/sentinel.conf && exec redis-sentinel /data/sentinel.conf"
]
volumes:
- ./conf/sentinel.conf:/usr/local/etc/redis/sentinel.conf
- ./data/s3:/data
ports:
- "26381:26379"
networks: [redisnet]
networks:
redisnet:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
sentinel.conf
port 26379
bind 0.0.0.0
protected-mode no
# 监控主库:mymaster 名称、主库 host/port、quorum(法定票数)
sentinel monitor mymaster 172.28.0.10 6379 2
# 认证(主库有 requirepass 时必须加)
sentinel auth-pass mymaster 123456
# 多久没响应算主观下线(ms)
sentinel down-after-milliseconds mymaster 5000
# 故障转移超时(ms)
sentinel failover-timeout mymaster 10000
# 同时同步副本数量(不影响本实验搭建,可保留默认)
sentinel parallel-syncs mymaster 1
# Redis 7 的 Sentinel 默认会把 monitor 的 host 当成“IP/可直接解析的实例”,
# 在某些环境下它不会走你以为的 resolver 路径(或要求显式开启 hostname 解析)
# 解决方法是——打开 Sentinel 的 hostname 解析开关
sentinel resolve-hostnames yes
sentinel announce-hostnames yes