【Redis 部署模式与数据结构与分布式锁全解】
一、Redis单机部署
1.1 架构
Client → Redis - 单节点
- 无高可用
- 无数据冗余
特点: - 架构简单
- 性能极高(单线程,纯内存)
1.2 适用场景
- 开发/测试环境
- 小流量系统
- 非核心业务缓存(验证码、临时状态)
1.3 为什么生产不推荐Redis单机
- redis是内存数据库,进程一挂数据直接不可用
- 单点故障,无法自动恢复
- 无法支撑业务增长
二、Redis主从部署
2.1 架构
→ Slave1 Client → Master → Slave2 2.2 核心机制
- 主写、从读(读写分离)
- 数据从Master单向复制到Slave
- Slave不能写
2.3 数据同步原理
第一次同步(全量复制)
- Slave发送psync
- Master执行bgsave
- 生产RDB文件
- RDB发送给Slave
- Slave加载RDB
- 同步期间的写命令通过复制缓冲区补发
后续同步(增量复制) - 基于replication offset
- 只同步丢失的命令
场景:
- 缓存读多写少
- 配合业务做读扩展
三、哨兵模式(Sentinel)
3.1 架构
Sentinel Sentinel Sentinel ↓ ↓ ↓ Master ←→ Slave1 Slave2 3.2 哨兵的作用
- 监控Redis节点是否存活
- 自动选主
- 通知客户端新的Master
3.3 故障转移流程
- Sentinel 发现 Master下线
- 多个Sentinel 投票确认(防误判)
- 选一个Slave 升级 Master
- 其他Slave 指向新 Master
- 客户端感知新 Master
四、集群部署(Redis Cluster)
为了解决:容量 + 写性能 + 高可用
4.1 架构
Slot 0~16383 ├─ Master1 → Slave1 ├─ Master2 → Slave2 └─ Master3 → Slave3 4.2 核心设计
哈希槽(Slot)
- 共16384个槽
- key – >CRC16 --> Slot
- 每个Master负责一部分 Slot
4.3 写入流程
- 客户端计算key的slot
- 直接访问对应Master
- 不走代理
4.4 特点
- 水平扩展
- 多主写入
- 高可用
缺点: - 不支持跨slot事务
- 多key操作有限制
4.5 Hash tag
user:{1001}:name user:{1001}:age 当{}内相同时强制落在同一slot
五、Redis数据结构
Redis是 Key-Value形式,Value是数据结构的核心
5.1 String
特点:
- 最基础
- 最大512MB
- 底层是SDS简单动态字符串
使用场景: - 缓存对象JSON
- 计数器(INCR)
- 分布式锁(SET NX EX)
5.2 Hash
特点:
- key --> field --> value
- 小hash用ziplist / listpack
- 大hash用 hashtable
使用场景: - 用户信息
- 对象属性缓存
| 方案 | 优点 | 缺点 |
|---|---|---|
| String(JSON) | 操作简单 | 更新字段成本高 |
| Hash | 字段级更新 | key多一点 |
5.3 List
特点
- 双向链表 + 压缩列表
- 支持LPUSH / RPOP
使用场景
- 消息队列(不可靠)
- 时间线
- 最新N条记录
5.4 Set(无序集合)
特点
- 去重
- 底层:intset / hashtable
使用场景
- 点赞、关注
- 抽奖
- 共同好友(交集)
5.5 ZSet(有序集合)
特点
- score排序
- 底层:跳表(skiplist)
使用场景
- 排行榜
- 延迟队列
- 权重排序
5.6 Bitmap
本质是String的位操作
使用场景
- 签到
- 活跃用户统计
- UV统计
5.7 HyperLogLog
特点
- 基数统计
- 极低内存
- 有误差约0.81%
使用场景
- 日活、月活
5.8 Stram(Redis 5.0+)
特点
- 消息队列
- 消费组
- 支持ACK
对比List
| list | stream | |
|---|---|---|
| 消息丢失 | 可能 | 不易 |
| 消费组 | 无 | 有 |
| 可追溯 | 否 | 是 |
六、分布式锁
1.可重入锁
RLock lock = redissonClient.getLock("myLock"); lock.lock();try{// 业务逻辑}finally{ lock.unlock();}2.公平锁
RLock fairLock = redissonClient.getFairLock("fairLock"); fairLock.lock();3.读写锁
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock");RLock readLock = rwLock.readLock();RLock writeLock = rwLock.writeLock();4.锁续期
// 看门狗机制,默认30秒续期一次privatevoidscheduleExpirationRenewal(long threadId){// 每隔 internalLockLeaseTime/3 时间续期Timeout task = commandExecutor.getConnectionManager().newTimeout(newTimerTask(){@Overridepublicvoidrun(Timeout timeout)throwsException{// 续期逻辑renewExpiration();}}, internalLockLeaseTime /3,TimeUnit.MILLISECONDS);}解锁机制
-- 解锁 Lua 脚本 if(redis.call('hexists', KEYS[1], ARGV[3])==0) then return nil; end; local counter = redis.call('hincrby', KEYS[1], ARGV[3],-1);if(counter >0) then redis.call('pexpire', KEYS[1], ARGV[2]);return0;else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]);return1; end;return nil;七、Redis内存淘汰机制
内存不够了 → 淘汰机制 怎么淘汰 → 淘汰策略 淘汰不当 → 雪崩 / 击穿 / 穿透 redis不是无限内存,用满; 就必须踢数据。
触发条件:
used_memory > maxmemory 一旦超过,写命令可能失败或者触发淘汰策略。
1.redis淘汰策略
1.1 不淘汰
noeviction: 是redis默认的内存淘汰策略,当redis内存大盗最大限制maxmemory时,noeviction不会淘汰任何键,而是拒绝执行会占用更多内存的命令。不删除,直接报错。
1.2 对设置了过期时间的key动手
| 策略 | 含义 |
|---|---|
| volatile-lru | 淘汰最近最少使用的 |
| volatile-lfu | 淘汰最少使用频率的 |
| volatile-ttl | 淘汰剩余时间最短的 |
| volatile-random | 随机删除一个或多个键 |
1.3 全量key都可能被淘汰
| 策略 | 含义 |
|---|---|
| allkeys-lru | 淘汰全局最近最少使用的 |
| allkeys-lfu | 淘汰全局最少使用频率的 |
| allkeys-random | 从所有键中(无论是否设置过期时间)随机删除一个或多个键 |
推荐使用: allkeys-lfu 或 allkeys-lru
优点:
- 热点数据更不容易被踢
- 冷数据自然淘汰
- 对业务无感
1.4 LRU和LFU区别
1.lru:最近用过(但是不等于常用)
2.lfu:一直在用 = 真热点
- 长期热点:LFU
- 短期热点:LRU
1.5 redis发现过期key的方式
惰性删除 + 定期删除
1)定时删除:性能差一般不用
2)惰性删除:访问时才删
3)定期删除:每秒抽样删
八、缓存雪崩、击穿、穿透
1.雪崩:大量缓存同一时间失效,大量请求到数据库上
场景:
- TTL时间一样
- 0点集体过期
- 数据库崩了
解决方案:
1)过期时间随机:ttl = 1h + random(0~10min)
2)热点数据永不过期 + 异步刷新:expire = -1
3)限流 + 熔断 + 降级
4)redis主从 / 集群
雪崩的本质是缓存同时失效,解决的核心就是错峰 + 兜底
2.缓存击穿:某一个热点key失效,大量并发请求同时绕过缓存,直接打到数据库上
解决方案:
1)分布式锁:保证同一时间只有一个线程回源数据库
publicDashboardVOgetDashboard(Long projectId){String cacheKey ="project:dashboard:"+ projectId;String lockKey ="lock:project:dashboard:"+ projectId;// 1. 查缓存String cache = redisTemplate.opsForValue().get(cacheKey);if(StringUtils.hasText(cache)){return JSON.parseObject(cache,DashboardVO.class);}RLock lock = redissonClient.getLock(lockKey);boolean locked =false;try{// 2. 尝试获取锁(最多等 2 秒) locked = lock.tryLock(2,TimeUnit.SECONDS);if(locked){// 3. 双重检查 cache = redisTemplate.opsForValue().get(cacheKey);if(StringUtils.hasText(cache)){return JSON.parseObject(cache,DashboardVO.class);}// 4. 查 DBDashboardVO dashboard =queryFromDB(projectId);// 5. 写缓存 redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(dashboard),30,TimeUnit.SECONDS );return dashboard;}}catch(InterruptedException e){Thread.currentThread().interrupt();}finally{// 6. 安全释放锁if(locked && lock.isHeldByCurrentThread()){ lock.unlock();}}// 7. 没拿到锁,短暂休眠重试sleep(50);returngetDashboard(projectId);}2)逻辑过期,缓存永不过期:统计类高并发场景
publicDashboardVOgetDashboardWithLogicalExpire(Long projectId){String cacheKey ="project:dashboard:"+ projectId;String json = redisTemplate.opsForValue().get(cacheKey);if(!StringUtils.hasText(json)){returnnull;}RedisData<DashboardVO> redisData = JSON.parseObject(json,newTypeReference<>(){});DashboardVO dashboard = redisData.getData();// 1. 未逻辑过期,直接返回if(redisData.getExpireTime()>System.currentTimeMillis()){return dashboard;}// 2. 已过期,尝试异步刷新RLock lock = redissonClient.getLock("lock:dashboard:"+ projectId);if(lock.tryLock()){try{// 异步重建缓存 CACHE_REBUILD_EXECUTOR.submit(()->{DashboardVO newData =queryFromDB(projectId);RedisData<DashboardVO> newRedisData =newRedisData<>(); newRedisData.setData(newData); newRedisData.setExpireTime(System.currentTimeMillis()+TimeUnit.SECONDS.toMillis(30)); redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(newRedisData));});}finally{ lock.unlock();}}// 3. 返回旧数据return dashboard;}3.缓存穿透
请求一个数据库根本不存在的数据:恶意请求,参数乱传
解决方案:
- 参数校验 - 第一层防线
- 空值缓存 - 简单有效
- 布隆过滤器 - 生产必备
- 限流 - 兜底
1)空值缓存 + 布隆过滤器:布隆过滤器用于判断“这个 Key 是否可能存在”,不存在的请求,直接拦截,不查 Redis、不查 DB
publicDashboardVOgetDashboard(Long projectId){// 1. 布隆过滤器if(!bloomFilter.mightContain(projectId)){returnnull;}String cacheKey ="project:dashboard:"+ projectId;String cache = redisTemplate.opsForValue().get(cacheKey);// 2. 空值缓存if("NULL".equals(cache)){returnnull;}if(StringUtils.hasText(cache)){return JSON.parseObject(cache,DashboardVO.class);}DashboardVO dashboard =queryFromDB(projectId);if(dashboard ==null){ redisTemplate.opsForValue().set(cacheKey,"NULL",5,TimeUnit.MINUTES);returnnull;} redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(dashboard),30,TimeUnit.SECONDS );return dashboard;}