尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Redis 缓存与分布式系统详解

Redis 缓存与分布式系统详解 Java 高频面试题Redis 缓存与分布式系统详解附详细答案 目录Redis 基础架构五种基本数据类型持久化机制 RDB vs AOF缓存常见问题分布式锁实现集群模式对比消息队列应用经典面试真题一、Redis 基础架构 ⭐⭐⭐⭐⭐1.1 Redis 核心特性✓ 基于内存的键值存储 ✓ 支持多种数据结构 ✓ 单线程模型 (IO 多线程) ✓ 丰富的数据类型 ✓ 主从复制 ✓ 持久化 ✓ 事务支持 ✓ Pub/Sub发布订阅1.2 Redis 数据模型┌─────────────────────────────┐ │ Redis Server │ ├─────────────────────────────┤ │ ┌───────────────────────┐ │ │ │ Key-Value Store │ │ │ ├───────────────────────┤ │ │ │ String: user:1 │ │ │ │ Alice │ │ │ ├───────────────────────┤ │ │ │ Hash: user:info │ │ │ │ name: Alice │ │ │ │ age: 25 │ │ │ ├───────────────────────┤ │ │ │ List: user:friends │ │ │ │ [Bob, Cathy] │ │ │ ├───────────────────────┤ │ │ │ Set: user:hobbies │ │ │ │ [music, book] │ │ │ ├───────────────────────┤ │ │ │ ZSet: rank:list │ │ │ │ {score:100, val:A} │ │ │ └───────────────────────┘ │ └─────────────────────────────┘1.3 Redis 命令示例# String (字符串)SET key value# 设置键值GET key# 获取值INCR key# 自增EXPIRE key seconds# 设置过期时间# Hash (哈希表)HSET user:name alice# 设置字段HGET user name# 获取字段HGETALL user# 获取所有字段# List (列表)LPUSH mylist value# 左侧插入RPUSH mylist value# 右侧插入LPOP mylist# 左侧弹出LRANGE mylist0-1# 获取全部# Set (集合)SADD myset element# 添加元素SISMEMBER myset element# 判断成员SMEMBERS myset# 获取所有成员# ZSet (有序集合)ZADD scores100alice# 添加元素及其分数ZRANGE scores0-1# 排名查询二、五种基本数据类型 ⭐⭐⭐⭐⭐2.1 String 字符串// 应用场景:1.缓存常用数据 redis.set(user:1:name,张三);redis.get(user:1:name);2.分布式锁 redis.setnx(lock:userId,1);redis.expire(lock:userId,10,TimeUnit.SECONDS);3.计数器/限流 redis.incr(visit:ip:ip);redis.incr(api:call:api);4.位图统计 redis.setbit(user:login:2024-08-01,userId,1);// 标记已登录redis.bitcount(user:login:2024-08-01);// 统计人数// BITFIELD 原子操作多个整数redis.bitfield(counter).incrByUnsigned(8,100)// 8 位无符号数从 100 开始加.get(u4,4);// 读取低 4 位2.2 Hash 哈希// 应用场景:1.对象存储RedisHash(valueuser,timeToLive3600)publicclassUser{privateLongid;privateStringname;privateintage;}2.购物车HSETcart:{userId}:items product11HSETcart:{userId}:items product22HVALScart:{userId}:items3.分页列表HSETpage:user_list0{id:1,name:张三}HSETpage:user_list1{id:2,name:李四}4.计数统计HINCRBYactivity:vote:voter_1 count1HGETALLactivity:vote:*2.3 List 列表// 应用场景:1.消息队列LPUSHjob_queue task1BRPOPLPUSHjob_queue process_queue// Spring Data Redis 示例ListOperationsString,ObjectopsredisTemplate.opsForList();ops.leftPush(queue,task);2.最新通知LPUSHnotify_list notificationLTRIMnotify_list09// 只保留最新的 10 条3.实时排行榜ZADDranking1000player1ZREVRANKranking player1// LRange 实现分页LRANGEnotifications09// 第 1 页LRANGEnotifications1019// 第 2 页2.4 Set 集合// 应用场景:1.标签系统SADDarticle:1001tags:techSADDarticle:1001tags:aiSMEMBERSarticle:1001:tags2.共同好友SINTERfriends:alice friends:bob// 求交集SUNIONfriends:alice friends:cindy// 求并集3.抽奖系统SADDlottery_pool prize1 prize2 prize3SRANDMEMBERlottery_pool1// 随机抽取SREMlottery_pool picked_prize// 删除已抽中的奖品4.去重统计SCARDunique_ips// 统计不同 IP 数量2.5 ZSet 有序集合 ⭐最常用// 应用场景:1.热搜排行榜ZADDhot_search_rank10000AI 技术ZADDhot_search_rank8500云计算ZREVRANGEhot_search_rank09WITHSCORES--返回前10名带热度分2.游戏排行榜ZADDgame_ranking1500player1ZADDgame_ranking1200player2ZREVRANGEBYSCOREgame_rankingINF0WITHSCORES3.按时间排序Stringkeyevents:date;ZADDkey timestamp eventId;ZRANGEBYSCOREkey startTimestamp endTimestamp;4.TopN查询ZREVRANKleaderboard player1;// 获取排名ZSCOREleaderboard player1;// 获取分数// Lua 脚本实现原子性更新Stringscript local score tonumber(redis.call(zscore, KEYS[1], ARGV[1])) if score then redis.call(zincrby, KEYS[1], ARGV[2], ARGV[1]) return redis.call(zscore, KEYS[1], ARGV[1]) else redis.call(zadd, KEYS[1], ARGV[2], ARGV[1]) return redis.call(zscore, KEYS[1], ARGV[1]) end ;三、持久化机制 RDB vs AOF ⭐⭐⭐⭐⭐3.1 RDB 快照持久化# 配置方式save9001# 15 分钟至少 1 个 key 变化save30010# 5 分钟至少 10 个 key 变化save6010000# 1 分钟至少 10000 个 key 变化# 手动触发SAVE# 阻塞式保存BGSAVE# 异步后台保存# RDB 特点:优点: ✓ 恢复速度快(只需加载一个文件)✓ 适合备份全量数据 ✓ 性能更好(fork 子进程处理)缺点: ✗ 可能丢失最后一次快照后的数据 ✗ fork 时可能导致短暂停顿 ✗ 文件较大# RDB 文件格式:REDIS0009\x00...RDB_HEADER...DATA...\x003.2 AOF 追加日志# 配置选项appendonlyyesappendfsync everysec# 每秒同步一次 (推荐)appendfsync always# 每次写入都同步 (安全但慢)appendfsync no# OS 控制同步 (最快)# AOF 重写机制BGREWRITEAOF# 异步重写 AOF 文件# AOF 重写原理:1. 遍历当前数据集2. 生成最小化的写命令3. 替换旧 AOF 文件# AOF 格式示例:*3$3SET$4key1$5value1# AOF 特点:优点: ✓ 数据安全性高(可配每写必存)✓ 可追朔完整操作历史 ✓ 支持增量重写 缺点: ✗ 文件体积大 ✗ 恢复速度较慢 ✗ 性能低于 RDB3.3 RDB vs AOF 对比维度RDBAOF数据完整性可能丢失几乎不丢恢复速度快 (秒级)慢 (分钟级)文件大小小大性能影响低中适用场景冷备/迁移热备/容灾3.4 最佳实践方案# 生产环境推荐配置rdb:enable:truesave-interval:300# 5 分钟keys-count:10# 10 个关键数据变化即保存aof:enable:trueappend-fsync:everysec# 平衡性能和安全性混合使用:├── RDB:快速恢复 异地备份├── AOF:数据保护 └── 定期测试恢复流程# 重启策略redis-server /etc/redis.conf--dir ./dump# 自动优先级: AOF RDB四、缓存常见问题 4.1 缓存穿透 ⚠️// 问题:查询不存在的数据请求直达数据库GetMapping(/user/{id})publicUsergetUser(PathVariableLongid){Userusercache.get(id);if(usernull){useruserMapper.selectById(id);// 空值也查cache.put(id,user);// 空值也缓存}returnuser;}// 攻击场景:恶意构造大量不存在的 ID → 击穿数据库GETuser:-1GETuser:-2GETuser:999999// ✅ 解决方案 1:缓存空对象if(usernull){cache.put(id,NULL_OBJECT,5*60);returnnull;}// ✅ 解决方案 2:布隆过滤器前置拦截publicbooleanexists(Longid){returnbloomFilter.exists(keyPrefix:id);}// ✅ 解决方案 3:接口层参数校验if(id1||idMAX_VALID_ID){thrownewIllegalArgumentException(非法 ID);}4.2 缓存雪崩 ⚠️// 问题:大量 key 同时过期或 Redis 宕机cache.put(key,value,1*60*60);// 固定过期时间// ❌ 风险:同一时刻全部失效// ✅ 解决方案:随机过期时间longrandomExpirebaseExpirerandom.nextInt(expireRange);cache.put(key,value,randomExpire);// ✅ 解决方案高可用集群部署// - Sentinel 哨兵模式// - Cluster 集群模式// - 主从切换 健康检查// ✅ 解决方案多级缓存LocalCache→RemoteCache→Database4.3 缓存击穿 ⚠️// 问题:热点 key 过期大量并发请求直击数据库GetMapping(/hot_product/{productId})publicProductgetProduct(PathVariableLongproductId){Stringkeyproduct:productId;Productproductcache.get(key);if(productnull){// 大量线程同时进入此分支productdb.query(productId);// DB 压力巨大cache.put(key,product);}returnproduct;}// ✅ 解决方案 1:互斥锁if(productnull){StringlockKeylock:key;RLocklockredisson.getLock(lockKey);if(lock.tryLock(0,3,TimeUnit.SECONDS)){try{// 双重检查if((productcache.get(key))null){productdb.query(productId);cache.put(key,product,1*60*60);}}finally{lock.unlock();}}else{Thread.sleep(50);// 重试returncache.get(key);}}// ✅ 解决方案 2:逻辑过期 (不设 TTL手动管理)data.withExpireTime(nowexpireTime).setValue(obj);4.4 缓存一致性难题// 场景:DB 数据变更如何保证缓存一致// 方案 1:先删缓存再更新 DB ❌ 有竞态条件TransactionalpublicvoidupdateUser(Useruser){cache.delete(user:user.getId());userMapper.update(user);}// 问题更新过程中读请求会读到旧缓存// 方案 2:先更新 DB再删缓存 ✅ 推荐TransactionalpublicvoidupdateUser(Useruser){userMapper.update(user);cache.delete(user:user.getId());}// 问题极端情况下可能不一致// 方案 3:延迟双删publicvoidupdateUser(Useruser){userMapper.update(user);cache.delete(user:user.getId());Thread.sleep(100);// 等待 binlog 同步cache.delete(user:user.getId());// 再次删除}// 方案 4:Canal MQ → 最终一致性userMapper.update(user);// Canal 监听 binlog → MQ 消费 → 删除缓存五、分布式锁实现 ⭐⭐⭐⭐⭐5.1 Redis 分布式锁基础// 简单实现publicclassSimpleRedisLockimplementsLock{privateStringkey;privateRedisTemplateredisTemplate;publicvoidlock(){BooleansuccessredisTemplate.opsForValue().setIfAbsent(key,1,10,TimeUnit.SECONDS);if(!success){thrownewRuntimeException(获取锁失败);}}publicvoidunlock(){// ❌ 直接删除不安全// 如果其他线程持有锁会被误删redisTemplate.delete(key);}}5.2 Redlock 算法publicclassDistributedLock{privatestaticfinalLongLOCK_SUCCESSOK.hashCode();privatestaticfinalStringSET_IF_NOT_EXISTNX;privatestaticfinalStringSET_WITH_EXPIRE_TIMEPX;/** * 尝试加锁 */publicbooleantryLock(StringlockKey,StringrequestId,longwaitTimeout,longleaseTime){try{BooleanresultredisTemplate.execute((RedisCallbackBoolean)connection-connection.setex(lockKey.getBytes(),(int)(leaseTime),requestId.getBytes()));returnBoolean.TRUE.equals(result);}catch(Exceptione){returnfalse;}}/** * 释放锁 */publicvoidunlock(StringlockKey,StringrequestId){Stringscript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end ;redisTemplate.execute(newDefaultRedisScript(script,Long.class),Collections.singletonList(lockKey),requestId);}}5.3 Redisson 实现 ⭐推荐// 使用 Redisson 分布式锁RLocklockredisson.getLock(myLock);// 1. 加锁 (默认等待时间租约时间)lock.lock();// 2. 指定等待时间和续期时间lock.lock(5,10,TimeUnit.SECONDS);// 3. 可中断加锁lock.lockInterruptibly(5,TimeUnit.SECONDS);// 4. 尝试加锁booleanisLockedlock.tryLock(5,10,TimeUnit.SECONDS);// 5. 解锁lock.unlock();// 6. 看门狗机制 (自动续期)// 不传 leaseTime 参数时Redisson 会自动续期rlong.addListener(newMessageListenerLong(){OverridepublicvoidonMessage(Longchannel,Longmessage){// 业务逻辑}});5.4 分布式锁注意事项// ✅ 正确做法:// 1. 必须设置过期时间 (防止死锁)lock.lockWithLeaseTime(10,TimeUnit.SECONDS);// 2. 解锁时必须验证所有权 (Lua 脚本)// 3. 考虑网络分区 (Redlock 算法)// 4. 考虑时钟跳变 (滑动时间窗口)// ❌ 错误做法:// 1. 忘记设置过期时间// 2. 不加锁直接操作共享资源// 3. 不使用原子性命令解锁// 4. 忽略异常场景六、集群模式对比 ⭐⭐⭐⭐⭐6.1 主从复制Master:读写分离 (主节点负责写所有节点都可以读) ↓ 复制 Slave1:只读 ↓ 复制 Slave2:只读 ↓ 复制 Slave3:只读 # 主从模式: - 读写分离 (提高读性能) - 故障转移 - 数据冗余 # 缺点: - 不支持水平扩展 - 单点故障6.2 哨兵模式 (Sentinel)Sentinel1 ↑↓ Sentinel2 ← Master (主节点) ↑↓ ↓ Sentinel3 Slave1 ↓ Slave2 功能: 1. 监控检测 Master 是否存活 2. 通知向管理员发送告警 3. 故障转移自动将 Slave 提升为 Master 4. 客户端配置提供新 Master 地址6.3 Redis Cluster (官方集群)Cluster 集群架构图: ┌──────────────────────────────────────┐ │ Slots (16384 个槽位) │ │ │ │ Node1(0-5000) Node2(5001-10000)│ │ ↓ ↓ │ │ Node3(10001-15000) │ └──────────────────────────────────────┘ 特点: ✓ 去中心化每个节点平等 ✓ 数据分片 (Sharding) ✓ 自动故障转移 ✓ 支持水平扩展 ✓ Gossip 协议通信6.4 三种模式对比模式优点缺点适用场景主从简单易懂无故障自动转移中小规模哨兵自动故障转移不能自动扩容读写分离Cluster可扩展、高可用复杂度高大规模生产七、消息队列应用7.1 Redis Streams// 创建 StreamXADDmystream*field1 value1 field2 value2// 消费数据XREADCOUNT10STREAMSmystream0-0// Spring Data Redis Streams 示例StreamOperationsString,ObjectopsredisTemplate.opsForStream();ConsumerGroupgroupnewConsumerGroup(mystream,consumers);// 生产者MapObject,ObjectdatanewHashMap();data.put(message,hello);DataFieldfieldnewDataField(data);StringmessageIdops.add(streamKey,field).getBody();// 消费者PageObjectRecordString,Objectrecordsops.read(group,Limit.limit(10));7.2 List 实现简单队列// 生产者LPUSHqueue_key item_value// 消费者RPOPqueue_key// 阻塞式 BRPOP// Spring 配置BeanpublicQueuesimpleQueue(){returnnewQueueBuilder().withName(simpleQueue).build();}// 使用广播模式SADDsubscribers subscriber1PUBLISHchannelmessageSUBSCRIBEchannel八、经典面试真题 Q1: Redis 为什么这么快A:答:Redis 高性能的原因: 1. 纯内存操作 └── 所有数据都在内存中没有磁盘 IO 2. 单线程模型 (CPU 密集型) └── 避免上下文切换 └── 避免竞争条件 3. 高效的 I/O 多路复用 └── Epoll 实现 └── 非阻塞 I/O 4. 精简的数据结构 └── SDS、跳过表等自定义结构 └── 避免不必要的内存分配 5. 内核优化 └── Zero-Copy └── 文件描述符批量处理 综合因素QPS 可达 10WQ2: Redis 如何实现分布式锁A:核心要点: 1. setnx 原子性设置 └── 只在未设置时才成功 2. 设置过期时间 └── 防止死锁 3. 解锁用 Lua 脚本 └── 验证锁的所有权 4. 考虑时钟回拨 └── Redlock 算法解决 5. 推荐使用现成框架 └── Redisson (成熟稳定) 代码示例: RLock lock redisson.getLock(myLock); lock.lock(5, 10, TimeUnit.SECONDS); try { // 业务逻辑 } finally { lock.unlock(); }Q3:如何解决缓存穿透、雪崩、击穿A:缓存穿透: 1. 缓存空对象 2. 布隆过滤器前置拦截 3. 接口层参数校验 缓存雪崩: 1. 随机过期时间 2. 高可用集群部署 3. 多级缓存 4. 限流降级 缓存击穿: 1. 互斥锁 (Mutex Lock) 2. 逻辑过期 (不设置 TTL) 3. 预热缓存 (启动时加载热点数据) 关键点: - 穿透:查不存在的数据 - 雪崩:大量 key 同时失效 - 击穿:单个热点 key 失效 参考资料《Redis 设计与实现》第 3 版Redis 官方文档Spring Boot 数据访问Alibaba Java 开发手册 更多内容持续更新中…关注我获取更多技术干货如果觉得有用欢迎点赞收藏转发有任何问题欢迎评论区交流~
返回列表