面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月31日 02:05

Redis 底层为什么这么快?核心数据结构如何支撑?

Redis 底层之所以快,主要靠三件事:数据大多在内存里,命令执行路径足够短,核心数据结构针对常见操作做了大量取舍。它不是简单的“单线程所以快”,而是 SDS、dict、skiplist、quicklist、intset、事件循环、I/O 多路复用和内存分配共同配合的结果。面试里回答这个问题,最好从数据结构讲到网络模型,再落到性能边界。字符串和哈希表是基础Redis 的 String 底层不是直接使用 C 字符串,而是 SDS。SDS 记录了长度和容量,获取长度是 O(1),扩容时有预分配策略,还能安全存储二进制数据。它的取舍是多占一点元数据空间,换来更安全的修改和更少的内存重分配。Hash、数据库 key 空间、过期字典大量依赖 dict。dict 使用哈希表加链地址法解决冲突,并通过两个哈希表做渐进式 rehash。这样不会在扩容时一次性搬完整张表,避免服务长时间卡住。踩坑点是 rehash 期间查询、写入都要同时照顾两张表,理解这一点才能解释为什么 Redis 能边服务边扩容。typedef struct dict { dictht ht[2]; long rehashidx;} dict;List、Set、ZSet 会按规模切换编码Redis 不会一上来就用最通用但最重的数据结构。小集合会用 intset 或紧凑编码节省内存,元素变多或类型变化后再升级。List 在新版本主要基于 quicklist,它把多个紧凑节点串成双向链表,兼顾顺序访问和内存占用。ZSet 常见实现是 dict + skiplist:dict 保证按 member 快速查分数,skiplist 保证按 score 范围查询和排名。跳表不是唯一能做有序结构的方案,平衡树也可以。Redis 选择跳表,是因为实现相对简单,范围查询自然,插入删除平均 O(log n),调试和维护成本低。代价是多层索引会额外占内存,score 相同还要按 member 做字典序比较。单线程命令执行不等于只有一个线程Redis 的核心命令执行长期保持单线程,这能避免复杂锁竞争,也让数据结构操作更可控。但网络 I/O、后台删除、AOF 刷盘、持久化重写等任务可能由其他线程或子进程参与。Redis 6 之后支持 I/O 多线程,主要优化网络读写,命令执行仍以单线程为主。redis-cli INFO memoryredis-cli SLOWLOG GET 10redis-cli --bigkeys -h 127.0.0.1 -p 6379事件循环和过期删除决定稳定性Redis 用事件循环处理文件事件和时间事件。文件事件负责连接读写,时间事件负责定时任务,比如过期 key 抽样删除、统计维护、复制心跳。过期删除不是每个 key 到点立刻删除,而是惰性删除加定期抽样删除。这个设计节省 CPU,但也意味着大量过期 key 可能短时间占住内存,需要合理设置 TTL 分布。追问Redis 为什么用 SDS,不直接用 C 字符串?C 字符串没有长度字段,求长度需要遍历,拼接时还容易写越界。SDS 把长度、剩余容量和字节数组放在一起,常见修改能先检查容量,不够再扩容。它的边界是会增加少量结构体开销,但 Redis 更在意高频操作的稳定延迟。存二进制内容时,SDS 也不会因为中间出现 \0 就截断。渐进式 rehash 解决了什么问题?如果哈希表扩容时一次性迁移所有 key,Redis 会在这一瞬间明显阻塞。渐进式 rehash 把迁移分摊到后续命令和定时任务里,每次搬一小部分。代价是 rehash 期间要查两张表,代码复杂一些,内存也会短暂增加。这个取舍很典型:用一点空间和实现复杂度,换线上延迟更平滑。ZSet 为什么通常用 dict 加 skiplist?dict 让 ZSCORE 这类按成员查分数的操作接近 O(1),skiplist 让按分数范围查询、排名查询保持 O(log n)。如果只用 dict,没法高效排序;如果只用 skiplist,按 member 查找又不够快。踩坑点是 ZSet 的内存成本比普通 Set 高很多,不适合无限堆用户行为明细。排行榜要加时间维度和过期策略,否则迟早变成大 key。Redis 单线程会不会被慢命令拖垮?会,所以生产环境要避免 KEYS、超大集合 SMEMBERS、大 key 删除这类操作。单线程的好处是没有锁竞争,坏处是一个慢命令就可能挡住后面的请求。线上更推荐 SCAN 分批扫描,用 UNLINK 异步删除大 key,并打开慢日志观察异常命令。Redis 很快,但前提是每个命令都足够短。怎么从底层角度排查 Redis 性能问题?先看 INFO memory 判断内存、碎片率和淘汰情况,再看 SLOWLOG 找慢命令。怀疑大 key 时用 --bigkeys 或离线 RDB 分析,怀疑网络瓶颈时看连接数、客户端输出缓冲区和命令 QPS。不要一上来就调线程数,很多问题其实是 key 设计或命令复杂度错了。底层原理的价值就在这里:能把“Redis 慢了”拆成结构、网络、内存和持久化几个方向。
服务端阅读 05月31日 02:05

Redis 常见应用场景有哪些?项目里该怎么选?

Redis 最常见的应用场景不是“把数据放进内存”这么简单,而是用它的低延迟、原子命令和多种数据结构,去补数据库、应用进程和消息系统的短板。项目里常见用法包括缓存、分布式锁、排行榜、计数器、限流、会话存储、轻量消息队列、地理位置和集合关系计算。真正要答好这个问题,重点不是背场景清单,而是说清楚每个场景为什么适合 Redis,以及什么时候不该用。缓存是最高频场景缓存通常用 Cache-Aside 模式:读请求先查 Redis,未命中再查数据库并写回缓存;写请求先更新数据库,再删除缓存。这个模式简单,但边界很多:不存在的数据要防缓存穿透,热点 key 过期要防击穿,大量 key 同时过期要防雪崩。实际项目里我会给 TTL 加随机抖动,并对空结果设置较短缓存时间。User user = redis.get("user:" + id);if (user == null) { user = db.queryById(id); redis.set("user:" + id, user, 3600 + random(300));}return user;分布式锁和限流要看一致性要求分布式锁常用 SET key value NX EX seconds,释放时用 Lua 比较 value 后再删除,避免删掉别人刚拿到的锁。它适合防重复提交、定时任务抢占、库存扣减前置保护,但不适合承诺绝对强一致;如果锁过期时间估短,业务没跑完锁就释放,会出现并发穿透。限流则常用 INCR、Sorted Set 滑动窗口或 Lua 令牌桶,固定窗口最简单,但窗口边界会有突刺。排行榜、计数器和集合计算是数据结构优势排行榜用 Sorted Set,ZADD 写分数,ZREVRANGE 查榜单,ZREVRANK 查名次,比数据库反复排序轻很多。阅读量、点赞数可以用 INCR 做原子计数,再异步落库。共同关注、标签筛选、用户分组适合用 Set 的交集和并集,但集合太大时要注意阻塞风险,线上不要随手对超大 key 做全量运算。ZADD article:rank 1024 article_1001ZREVRANGE article:rank 0 9 WITHSCORESSINTER user:1:follows user:2:follows消息队列和会话存储要谨慎取舍简单异步任务可以用 List 的 LPUSH + BRPOP,Redis Stream 支持消费组和确认机制,更像轻量队列。它的好处是接入快、延迟低;边界是消息堆积、重试治理和跨机房容灾能力不如 Kafka、RocketMQ 这类专业消息系统。Session 存 Redis 很常见,适合多实例共享登录态,但敏感字段要加密或只存引用 ID,不能把 Redis 当成无边界的安全仓库。还有一个常被忽略的点:Redis 不是持久化数据库的替代品,AOF 和 RDB 能降低丢数据概率,却不能让所有缓存场景天然具备事务语义。设计时要先问清楚“丢一小段数据能不能接受”,再决定是否把它放在 Redis 里,这个判断很关键。追问Redis 做缓存时,怎么处理缓存和数据库不一致?常见做法是先更新数据库,再删除缓存,让下一次读取重新加载新值。这个方案牺牲了一点短时间一致性,换来实现简单和故障恢复容易。踩坑点是删除缓存失败会留下旧值,所以生产里通常配合消息重试、binlog 订阅或短 TTL 兜底。如果业务要求读到最新值,比如支付状态,就不要读缓存,直接读主库或走强一致链路。Redis 做分布式锁可靠吗?单 Redis 节点的锁只能解决大多数工程并发问题,不能等同于严格分布式共识。锁 value 必须是唯一 token,释放锁必须用 Lua 原子校验,否则会误删别人的锁。Redisson 的看门狗能降低锁过期风险,但如果进程 STW、网络抖动或 Redis 主从切换,仍要考虑幂等和补偿。重要结论是锁只能减少并发冲突,不能替代业务层一致性设计。Redis Stream 能不能替代 Kafka?低吞吐、少团队协作、希望快速落地的内部任务队列可以用 Redis Stream。它支持消费组、ACK 和消息 ID,比 Pub/Sub 可靠,但在超大吞吐、长期消息保留、多分区扩展和生态工具上不如 Kafka。项目里如果消息是核心链路,优先选专业 MQ;如果只是削峰、异步发通知或刷新缓存,Stream 够用而且维护成本低。热点 key 和大 key 分别怎么处理?热点 key 是访问太集中,可以用本地缓存、多副本读、key 分片或提前预热来分散压力。大 key 是单个 value 或集合过大,会导致网络传输慢、删除阻塞、主从同步卡顿,应该拆分存储并用 UNLINK 异步删除。两个问题经常混在一起,但处理方向不同:热点 key 关注流量,大 key 关注体积。上线前用 redis-cli --bigkeys 和慢日志排查,比故障后临时猜要靠谱。
服务端阅读 05月31日 02:05

Redis 常见问题怎么排查?大 Key、热点和一致性如何处理?

Redis 常见问题不要按概念清单排查,线上通常先看现象:延迟升高、内存暴涨、从库追不上、数据库被打穿、某个接口 QPS 异常。高频原因包括大 Key、热点 Key、慢命令、缓存一致性、分布式锁误用和限流算法选错。Redis 很快,但单个大对象、阻塞命令、网络抖动和错误过期策略,都会把系统拖慢。追问Redis 为什么快,慢请求先看什么?Redis 快在内存访问、单线程命令执行避免锁竞争、I/O 多路复用处理连接,以及高效数据结构。Redis 6 之后网络 I/O 可以多线程,但命令执行仍是核心单线程路径,一个慢命令会拖住后面的请求。排查先看 SLOWLOG GET、INFO commandstats、LATENCY DOCTOR,不要只盯 CPU;很多延迟其实来自阻塞命令、fork、AOF fsync、主从同步或客户端连接池耗尽。大 Key 怎么发现,为什么不能直接 DEL?大 Key 可能是几 MB 的字符串,也可能是几十万元素的 hash、list、set,可以用 redis-cli --bigkeys 抽样扫描,并在业务侧记录 value 大小。生产环境不要随手 KEYS *,它本身就可能阻塞实例。大 Key 的读写、序列化、网络传输和删除都可能卡主线程;删除时优先用 UNLINK 或后台渐进清理,核心集群要放在低峰并评估影响。热点 Key 如何处理,复制多份一定安全吗?热点 Key 是某个 key 被大量并发访问,常见于秒杀库存、首页配置、热门商品详情。只读热点可以用本地缓存、读写分离、客户端缓存,或复制多份 key 随机读取。写热点不能简单复制,否则一致性会很难保证;可累加计数可以分片后异步汇总,强一致库存更适合排队、限流或业务层削峰。缓存和数据库不一致怎么处理?常用 Cache Aside 是读缓存,未命中查数据库再写缓存;写数据时先更新数据库,再删除缓存。它简单可靠,适合大多数最终一致场景,但并发读写下仍可能短暂不一致。延迟双删能降低概率却不好选延迟时间,binlog 或消息队列更稳但链路更复杂;余额、库存扣减这类强一致路径不要依赖缓存读。分布式锁和限流最容易踩哪些坑?Redis 锁至少要用 SET key value NX EX seconds,释放时用 Lua 先比较 value 再删除,避免删掉别人的锁。过期时间太短会导致业务没跑完锁已释放,太长又影响故障恢复,Redisson 看门狗能缓解但不能无视 GC 暂停和网络分区。限流里固定窗口简单但有边界突刺,滑动窗口更平滑但占内存,令牌桶适合允许短时突发;无论哪种都要配合幂等和降级。写段命令和配置redis-cli SLOWLOG GET 10redis-cli --latency -h 127.0.0.1 -p 6379redis-cli LATENCY DOCTORredis-cli --bigkeysredis-cli INFO memoryredis-cli INFO commandstats// 安全释放分布式锁:比较 value 后再删除String lua = "if redis.call('GET', KEYS[1]) == ARGV[1] " + "then return redis.call('DEL', KEYS[1]) else return 0 end";redis.eval(lua, Collections.singletonList(lockKey), Collections.singletonList(lockValue));// Cache Aside:先更新数据库,再删除缓存public void updateUser(User user) { db.update(user); redis.del("user:" + user.getId());}小结Redis 排查要从现象回到机制:慢请求看阻塞和慢命令,内存问题看大 Key 和淘汰策略,流量尖峰看热点 Key,数据异常看缓存一致性链路。解决方案都在性能、一致性、复杂度之间取舍,提前做好 key 规模约束、慢日志监控、过期时间打散和降级预案,比报警后临时救火可靠。
服务端阅读 05月28日 09:38

Redis 安全配置怎么做?生产环境加固 checklist 与漏洞防范

核心回答Redis 安全配置要从网络隔离、认证授权、数据保护和运行加固四个层面入手。生产环境最低要求三条:绑定内网 IP + 开启密码认证 + 禁用危险命令。做不到这三条,Redis 基本等于裸奔。2015 年爆发的 Redis 未授权访问漏洞(CVE-2015-4335)让大量服务器被植入挖矿脚本和 SSH 公钥,根源就是默认配置下 Redis 无密码监听所有网卡。这个漏洞至今仍在被批量扫描利用,绝不是历史问题。每次安全加固的第一步,就是确保这三条底线全部到位。网络层隔离绑定监听地址Redis 默认绑定 0.0.0.0,所有网卡都能连,这是最常见的安全隐患。修改 redis.conf:bind 127.0.0.1 10.0.0.1protected-mode yesprotected-mode 是 Redis 3.2 引入的保护机制,当 Redis 绑定了非回环地址且没有设置密码时,会拒绝外部连接。这个开关一定要保持开启。生产环境建议只绑定内网 IP,绝不要直接暴露到公网。如果必须远程访问,走 VPN 或 SSH 隧道。很多 Redis 被入侵的案例就是因为公网暴露了 6379 端口,被扫描器批量发现后利用。防火墙规则即使绑定了内网 IP,也要用防火墙做二次防护,这是纵深防御的基本思路:# iptables 方式iptables -A INPUT -p tcp --dport 6379 -s 10.0.0.0/24 -j ACCEPTiptables -A INPUT -p tcp --dport 6379 -j DROP# firewalld 方式firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/24" port protocol="tcp" port="6379" accept'firewall-cmd --reload云上环境用安全组实现同样的效果,原则是端口最少放开。AWS、阿里云的安全组规则中,Redis 端口只对应用服务器所在子网开放。TLS 加密传输Redis 6.0 开始原生支持 TLS,这是生产环境安全加固的重要一环。如果数据经过不可信网络(跨机房、公网),必须开启:# 生成证书openssl genrsa -out redis.key 2048openssl req -new -key redis.key -out redis.csropenssl x509 -req -days 365 -in redis.csr -signkey redis.key -out redis.crt# redis.conf 配置tls-port 6380port 0tls-cert-file /path/to/redis.crttls-key-file /path/to/redis.keytls-ca-cert-file /path/to/ca.crt设置 port 0 关闭明文端口,强制所有连接走 TLS。集群模式下还需要配置 tls-cluster yes 和 tls-replication yes。Redis 7.0 进一步增强了 TLS 支持,集群总线通信也可以走加密通道。认证与授权密码认证最基本的安全措施,也是 Redis 安全加固 checklist 的第一条:# redis.confrequirepass your_strong_password# 运行时设置(重启失效)CONFIG SET requirepass your_strong_password# 连接时指定密码redis-cli -a your_strong_password注意:-a 参数会触发警告,密码可能出现在进程列表和日志中。建议用 REDISCLI_AUTH 环境变量代替:export REDISCLI_AUTH=your_strong_passwordredis-cli密码强度要求:至少 16 位,混合大小写字母、数字和特殊字符。requirepass 的密码以明文存储在配置文件中,所以配置文件权限也要收紧。ACL 精细权限控制Redis 6.0 引入 ACL(Access Control List),替代了之前只有一个全局密码的模式,是实现 Redis 安全最小权限原则的关键:# 创建只读用户,只能访问 user: 开头的 keyACL SETUSER readonly on >password1 ~user:* +@read# 创建业务用户,只能操作特定前缀的 keyACL SETUSER app_user on >app_password ~order:* +@read +@write +@string +@hash# 创建管理员ACL SETUSER admin on >admin_password ~* +@all# 查看所有用户ACL LIST# 删除用户ACL DELUSER readonlyACL 的权限粒度可以精确到命令组和 key 模式。+@read 表示所有读命令,+@write 表示所有写命令,+@all 表示全部命令。用 ACL CAT 查看所有命令组。实际部署中,为每个业务应用创建独立的 ACL 用户,遵循最小权限原则。一个只做缓存的业务不需要 FLUSHALL 权限。Redis 7.0 还支持 ACL 规则持久化到文件,通过 aclfile 配置项指定,比每次重启都重新配置更可靠。禁用和重命名危险命令这是防止 Redis 被攻击者利用的关键配置:# redis.confrename-command FLUSHALL ""rename-command FLUSHDB ""rename-command CONFIG ""rename-command SHUTDOWN ""rename-command DEBUG ""# 或者重命名为难猜的名字rename-command FLUSHALL "a9b8c7d6e5_FLUSHALL"禁用比重命名更安全。如果用重命名,新的命令名不要出现在代码和日志中。CONFIG 命令尤其危险,攻击者可以通过它修改 requirepass 实现持久化后门——先把密码改成自己知道的值,再修改 dir 指向 /root/.ssh/,dbfilename 设为 authorized_keys,执行 BGSAVE 写入 SSH 公钥。这就是 CVE-2015-4335 的经典攻击链。数据安全持久化策略# RDB 快照save 900 1save 300 10save 60 10000# AOF 日志appendonly yesappendfsync everysecRDB 适合做备份,AOF 适合做数据安全。生产环境建议两者都开,AOF 保证最多丢 1 秒数据,RDB 做快速恢复的兜底。注意:AOF 文件可能包含敏感数据(如密码明文),需要控制文件访问权限。加密持久化文件Redis 本身不提供数据加密,需要在文件系统层面解决:# 限制文件权限chmod 700 /var/lib/redischmod 600 /var/lib/redis/dump.rdbchmod 600 /var/lib/redis/appendonly.aof# 文件系统加密(Linux)# 使用 LUKS 或 eCryptfs 加密 Redis 数据目录如果数据敏感性高(如用户信息、Token),在写入 Redis 前做应用层加密。读取时解密,Redis 只存密文。这样即使持久化文件被窃取,也无法直接获取明文数据。备份与恢复# 定时备份 RDB0 2 * * * cp /var/lib/redis/dump.rdb /backup/dump_$(date +\%Y\%m\%d).rdb# 备份到远程rsync -avz /backup/ user@remote:/backup/备份文件也要控制权限,最好加密后传输。恢复时注意检查 RDB 文件完整性,避免被篡改的备份文件引入恶意数据。用 redis-check-rdb 工具校验 RDB 文件,用 redis-check-aof 校验 AOF 文件。运行时加固最小权限运行# 创建专用用户useradd -r -s /bin/false redis# 设置文件归属chown -R redis:redis /var/lib/redischown redis:redis /etc/redis/redis.conf# 用非 root 用户启动sudo -u redis redis-server /etc/redis/redis.confRedis 不需要 root 权限。用 root 运行 Redis 一旦被攻破,攻击者可以直接拿到服务器控制权,这就是为什么 Redis 安全加固必须包含权限降级。文件权限收紧chmod 600 /etc/redis/redis.confchmod 700 /var/lib/redischmod 600 /var/log/redis/redis.log配置文件包含密码等敏感信息,必须限制读写权限。日志文件可能包含查询内容,也要保护。系统级隔离用 systemd 的安全选项加强隔离:[Service]User=redisGroup=redisExecStart=/usr/bin/redis-server /etc/redis/redis.confProtectSystem=fullReadWritePaths=/var/lib/redisNoNewPrivileges=truePrivateTmp=trueNoNewPrivileges=true 防止子进程提权,PrivateTmp=true 隔离临时目录。Docker 部署时同理,不要用 --privileged 参数,用 --user 指定非 root 用户,用 --cap-drop=ALL 去掉不必要的 Linux capabilities。监控与审计慢查询监控# redis.confslowlog-log-slower-than 10000slowlog-max-len 128# 查看慢查询SLOWLOG GET 10慢查询日志可以帮助发现异常操作。突然出现的慢查询可能是攻击者在执行大量 KEYS * 扫描或 DEL 删除。Prometheus + Grafana 监控# prometheus.ymlscrape_configs: - job_name: 'redis' static_configs: - targets: ['localhost:9121']# 告警规则groups: - name: redis_alerts rules: - alert: RedisTooManyConnections expr: redis_connected_clients > 100 for: 1m labels: severity: warning - alert: RedisSuspiciousCommands expr: rate(redis_commands_processed_total[5m]) > 10000 for: 2m labels: severity: critical关键监控指标:连接数突增、内存使用异常、命令执行频率异常、主从切换。连接数异常暴增可能是在做端口扫描或暴力破解密码,命令频率异常可能是攻击者在批量导出数据。操作审计Redis 本身的审计能力有限,建议在以下层面补充:网络层:记录所有连接来源 IP,排查可疑来源应用层:在业务代码中记录关键操作,特别是写入和删除操作系统层:用 auditd 监控 redis.conf 文件变更,防止配置被篡改如果合规要求严格,可以考虑 Redis 企业版的审计日志功能,或用第三方审计代理。集群安全主从复制认证# 主节点配置masterauth your_master_passwordrequirepass your_master_password# 从节点配置requirepass your_slave_passwordmasterauth your_master_password主从之间必须设置认证。没有认证的复制关系,攻击者可以伪装成从节点拉取全量数据,这相当于直接把数据库内容交给了攻击者。哨兵模式安全# sentinel.confsentinel auth-pass mymaster your_master_password哨兵也需要配置认证密码,否则攻击者可以操控哨兵触发故障转移,把主节点切换到自己控制的服务器,实现中间人攻击。集群模式安全Redis 7.0 开始支持集群 TLS,集群总线通信也走加密通道:cluster-enabled yescluster-config-file nodes.confcluster-node-timeout 5000tls-cluster yestls-replication yes集群模式下的安全比单机更复杂,因为节点间通信也需要保护。Redis 7.2+ 还支持集群总线端口的 TLS 认证,确保集群内部通信不被窃听。安全加固优先级按紧迫程度排序,这份 Redis 安全加固 checklist 可以直接参考:立即做:绑定内网 IP + 开启密码认证 + 禁用危险命令。不做这三条就是在等被入侵尽快做:配置防火墙 + 使用非 root 用户 + 收紧文件权限。降低被攻破后的影响范围逐步做:开启 TLS + 配置 ACL + 部署监控告警。提升整体安全水位持续做:更新版本修复 CVE + 审计日志 + 定期安全扫描。保持安全性不退化已知安全漏洞Redis 历史上几个重要的安全漏洞,面试和实战都会遇到:CVE-2015-4335:未授权访问写入 SSH 公钥和 cron 任务,这是最经典也最常被利用的 Redis 安全漏洞。攻击条件极其简单:Redis 暴露公网 + 无密码CVE-2022-0543:Debian/Ubuntu 打包的 Lua 沙箱逃逸,可以在 Redis 中执行任意代码。影响范围广,因为大部分 Linux 发行版都用系统包管理器安装 RedisCVE-2023-41053:Lua 脚本库堆栈溢出,可导致拒绝服务CVE-2025-32023:Redis 7.4.x 之前的 Lua 脚本 eval 逃逸漏洞,最新一轮安全修复面试中被问到 Redis 安全,提到 CVE-2015-4335 说明你理解问题的根源:默认配置不安全。提到 ACL 说明你跟进 Redis 6.0+ 新版本特性。提到加固优先级说明你有生产环境实战经验。应急响应发现 Redis 被入侵时的处理步骤:立即断网:iptables -A INPUT -p tcp --dport 6379 -j DROP,先止血保留现场:SLOWLOG GET 100、CLIENT LIST、INFO 记录当前状态,为后续分析保留证据检查后门:查 crontab、SSH authorized_keys、.bashrc 是否被篡改,这是 Redis 被入侵后最常见的持久化手段分析入侵路径:检查 CONFIG GET dir 和 CONFIG GET dbfilename 是否被改过,确认数据文件是否被指向了系统敏感目录清除恢复:在确保后门清除后,修改密码重启 Redis,从备份恢复数据加固复盘:按上面的安全加固 checklist 逐项检查,堵住入侵路径大多数 Redis 被入侵事件的根因都是:公网暴露 + 无密码 + CONFIG 命令可用。攻击者通过 CONFIG SET dir /root/.ssh/ CONFIG SET dbfilename authorized_keys 写入 SSH 公钥实现持久化控制。理解这个攻击链,就知道为什么禁用 CONFIG 命令是 Redis 安全加固的核心措施。
服务端阅读 05月28日 09:37

Redis缓存穿透、击穿、雪崩有什么区别?

Redis 缓存策略是使用 Redis 作为缓存时的核心问题,需要解决缓存穿透、缓存击穿、缓存雪崩等问题,同时需要设计合理的缓存更新策略。缓存穿透:查不存在的数据怎么办?缓存穿透是指查询一个数据库和缓存中都不存在的数据,请求每次都会穿过缓存直接打到数据库。典型场景:攻击者用不存在的 ID 批量请求接口,如 /user/-1。方案一:缓存空对象当数据库也查不到时,将空值写入缓存并设置短 TTL,避免同一 key 反复穿透。public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return "NULL".equals(user) ? null : user; } user = db.queryUserById(id); if (user == null) { redis.set("user:" + id, "NULL", 300); // 缓存空对象,5分钟过期 } else { redis.set("user:" + id, user, 3600); } return user;}注意:空对象 TTL 不宜过长,否则该 key 对应的真实数据写入后,缓存仍是空值,导致数据不一致。可结合主动删除策略,写入数据时同步删除空缓存。方案二:布隆过滤器在缓存前加一层布隆过滤器,快速判断 key 是否可能存在。布隆过滤器说不存在则一定不存在,说存在则可能存在(有误判率)。if (!bloomFilter.mightContain("user:" + id)) { return null; // 一定不存在,直接返回}User user = redis.get("user:" + id);if (user != null) { return user;}user = db.queryUserById(id);if (user != null) { redis.set("user:" + id, user, 3600);}return user;选型建议:数据量小且查询模式固定 → 缓存空对象更简单;数据量大且 key 空间稀疏 → 布隆过滤器更省内存。缓存击穿:热点 key 过期瞬间怎么办?缓存击穿是指某个热点 key 在过期的一瞬间,大量并发请求同时查询该 key,全部穿透到数据库。与雪崩的区别:击穿是单个热点 key,雪崩是大面积 key 同时失效。方案一:互斥锁用分布式锁保证只有一个线程查库并回填缓存,其他线程等待后重试。public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return user; } String lockKey = "lock:user:" + id; try { if (redis.setnx(lockKey, "1", 10)) { // 获取锁,10秒自动释放 user = db.queryUserById(id); redis.set("user:" + id, user, 3600); } else { Thread.sleep(100); return getUserById(id); // 等待后重试 } } finally { redis.del(lockKey); } return user;}注意:锁要设超时时间防止死锁;重试要设上限防止无限递归。方案二:逻辑过期缓存永不过期,在值中存一个逻辑过期时间戳。读到逻辑过期数据时,异步更新缓存,当前请求返回旧数据。牺牲短暂一致性换取高可用。public User getUserById(Long id) { String value = redis.get("user:" + id); if (value != null) { JSONObject json = JSON.parseObject(value); if (json.getBoolean("expired")) { asyncUpdateCache(id); // 异步更新,不阻塞当前请求 } return json.getObject("data", User.class); // 返回旧数据 } User user = db.queryUserById(id); JSONObject json = new JSONObject(); json.put("data", user); json.put("expired", false); redis.set("user:" + id, json.toJSONString(), 3600); return user;}选型建议:一致性要求高 → 互斥锁;可用性要求高、允许短暂脏读 → 逻辑过期。缓存雪崩:大面积 key 同时失效怎么办?缓存雪崩是指大量 key 在同一时间过期,或 Redis 宕机,导致请求全部打到数据库。方案一:过期时间加随机偏移给 TTL 加上随机值,避免大量 key 在同一时刻集中过期。int expire = 3600 + new Random().nextInt(600); // 3600~4200秒随机redis.set("user:" + id, user, expire);方案二:缓存预热系统启动或低峰期提前加载热点数据到缓存,避免冷启动时流量直接打库。@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点预热public void warmUpCache() { List<User> users = db.queryHotUsers(); for (User user : users) { int expire = 3600 + new Random().nextInt(600); redis.set("user:" + user.getId(), user, expire); }}方案三:高可用 + 降级部署 Redis Sentinel 或 Cluster,避免单点故障数据库前加熔断限流,雪崩时快速失败而非拖垮数据库使用本地缓存(Caffeine/Guava)作为二级缓存兜底public User getUserById(Long id) { try { User user = redis.get("user:" + id); if (user != null) { return user; } } catch (Exception e) { log.error("Redis error", e); return localCache.get("user:" + id); // 降级到本地缓存 } User user = db.queryUserById(id); redis.set("user:" + id, user, 3600); return user;}缓存更新策略怎么选?三种常见策略,适用场景不同:Cache Aside(旁路缓存):读时回填,写时删缓存。最常用,一致性较好。// 读User user = redis.get("user:" + id);if (user == null) { user = db.queryUserById(id); redis.set("user:" + id, user, 3600);}// 写db.updateUser(user);redis.del("user:" + user.getId()); // 删缓存而非更新缓存为什么删缓存而不是更新?并发写时更新缓存可能出现旧值覆盖新值的问题,删除更安全,下次读时自然回填最新值。Write Through(写穿透):写数据时同步更新缓存和数据库。数据一致性强,但写延迟高,适合写少读多的场景。Write Behind(写回):先更新缓存,异步批量写入数据库。写性能极高,但有数据丢失风险,适合写密集且容忍少量丢失的场景(如浏览量计数)。缓存和数据库不一致怎么处理?缓存与数据库不一致是分布式系统的经典问题,根本原因是两者无法原子操作。方案一:延时双删先删缓存 → 更新数据库 → 延时再删缓存。第二次删除用于清除更新数据库期间被旧缓存回填的数据。public void updateUser(User user) { redis.del("user:" + user.getId()); // 第一次删除 db.updateUser(user); Thread.sleep(500); // 延时,确保读请求回填旧缓存完成 redis.del("user:" + user.getId()); // 第二次删除}缺点:延时时间难以精确设定,过长影响性能,过短仍可能不一致。方案二:订阅 Binlog通过 Canal 等中间件订阅数据库 Binlog,数据变更时自动更新或删除缓存。解耦业务代码,一致性更可靠。@CanalEventListenerpublic class CacheUpdateListener { @ListenPoint(destination = "example", schema = "test", table = "user") public void onEvent(CanalEntry.Entry entry) { User user = parseUserFromBinlog(entry); redis.del("user:" + user.getId()); // 或更新缓存 }}选型建议:一致性要求一般 → 延时双删够用;一致性要求高 → Binlog 方案更可靠。面试怎么答?被问到这三个概念时,建议按以下结构回答:先说定义:穿透是数据不存在,击穿是热点 key 过期,雪崩是大面积 key 同时失效再说区别:穿透查的是不存在的数据,击穿和雪崩查的是存在的数据;击穿是单个 key,雪崩是批量 key最后说方案:穿透用空缓存或布隆过滤,击穿用互斥锁或逻辑过期,雪崩用随机过期+预热+高可用追问方向:生产环境怎么监控缓存健康度?关注缓存命中率(低于 80% 需告警)、慢查询日志、内存使用率和 key 过期分布。
服务端阅读 05月28日 09:37

Redis 如何进行监控和运维?

Redis 监控和运维是保障线上稳定性的核心能力,面试中常从"监控哪些指标""用什么工具""遇到问题怎么排查"三个角度考察。关键监控指标内存指标内存是 Redis 最核心的资源,重点监控以下项:INFO memory# 关键字段used_memory # Redis 分配器分配的内存used_memory_rss # 操作系统实际分配的内存mem_fragmentation_ratio # 内存碎片率 = used_memory_rss / used_memorymaxmemory # 配置的最大内存限制碎片率阈值解读:碎片率 > 1.5 说明碎片严重,需触发 MEMORY PURGE 或重启;碎片率 < 1.0 说明使用了 Swap,性能会急剧下降,应立即排查。命中率直接反映缓存有效性:keyspace_hits / (keyspace_hits + keyspace_misses)命中率低于 90% 时需排查是否有过期 key 未清理、缓存穿透等问题。性能与延迟指标INFO stats# instantaneous_ops_per_sec — 当前 QPS# total_commands_processed — 累计处理命令数# 延迟监控(Redis 2.8.13+)CONFIG SET latency-monitor-threshold 100 # 超过 100ms 记录LATENCY LATEST # 查看最近延迟事件LATENCY DOCTOR # 诊断延迟原因QPS 突降或延迟突增是故障的前兆,应设置基线告警。连接与复制指标INFO clients# connected_clients — 当前连接数# blocked_clients — 阻塞等待的客户端数INFO replication# master_repl_offset / slave_repl_offset — 主从复制偏移量差即同步延迟# master_link_down_since_seconds — 主从断连时长,应为 0连接数超过 maxclients 的 80% 就应告警;主从偏移量差持续增大说明同步瓶颈。持久化指标INFO persistence# rdb_last_save_time — 最后 RDB 保存时间# rdb_changes_since_last_save — 上次保存后变更数,过大说明 RDB 间隔过长# aof_rewrite_in_progress — AOF 重写是否进行中# aof_current_size / aof_base_size — AOF 文件大小,重写触发比默认 100%监控工具选型原生命令INFO:全局状态快照,按 section 查看内存、客户端、复制等SLOWLOG:慢查询日志,SLOWLOG GET 10 查看最近 10 条,关注 usec 字段MONITOR:实时命令流,生产环境慎用(会降低吞吐约 50%),仅用于紧急排查LATENCY:延迟监控框架,可记录延迟事件并给出诊断建议可观测性体系Prometheus + Redis Exporter + Grafana 是生产环境主流方案:# 部署 Redis Exporterdocker run -d --name redis-exporter \ -e REDIS_ADDR=redis://localhost:6379 \ prom/redis-exporter# Prometheus 抓取配置scrape_configs: - job_name: redis static_configs: - targets: ["localhost:9121"]Grafana 可导入 Redis Dashboard(ID: 11835),覆盖内存、QPS、命中率、延迟等核心面板。Redis Insight 是 Redis 官方可视化工具,支持内存分析、CLI、Profiler,适合开发调试阶段。哨兵监控使用 Sentinel 时,除上述指标外还需关注:SENTINEL masters # 主节点状态SENTINEL slaves <master> # 从节点状态SENTINEL sentinels <master> # 哨兵节点状态重点监控主观下线/客观下线事件、故障转移耗时。常见故障排查内存不足现象:OOM command not allowed when used memory > maxmemoryINFO memory # 查看内存使用redis-cli --bigkeys # 扫描大 KeyMEMORY USAGE <key> # 查看单个 key 内存占用解决:调整 maxmemory、设置淘汰策略 allkeys-lru、用 UNLINK 替代 DEL 异步删除大 Key(避免阻塞主线程)、分批 SCAN 删除。慢查询SLOWLOG GET 10 # 查看慢查询CONFIG GET slowlog-log-slower-than # 查看阈值常见原因:KEYS * 全量扫描、大 Key 操作(HGETALL 千万级 hash)、SORT 命令。替代方案:SCAN 替代 KEYS,HSCAN 替代 HGETALL,Pipeline 减少网络往返。主从同步延迟INFO replication# master_repl_offset 与 slave_repl_offset 的差值增大 repl-backlog-size、优化网络、避免主节点大 Key 写入导致积压。连接数打满INFO clientsCONFIG GET maxclients设置 timeout 自动断开空闲连接、排查连接泄漏、适当调大 maxclients。运维操作要点备份恢复RDB 备份用 BGSAVE(后台执行,不阻塞主线程),AOF 备份直接拷贝 .aof 文件。恢复时停止 Redis → 替换文件 → 启动。生产建议 RDB + AOF 混合持久化(Redis 4.0+)。数据迁移MIGRATE 命令:迁移单个或多个 key,原子操作RedisShake:阿里开源工具,支持全量+增量同步,适合大规模迁移# RedisShake 配置source.type: standalonesource.address: source.redis.com:6379target.type: standalonetarget.address: target.redis.com:6379./redis-shake.linux -type=sync -conf=shake.conf集群扩缩容# 添加节点redis-cli --cluster add-node <new-ip>:<port> <exist-ip>:<port># 重新分配槽位redis-cli --cluster reshard <exist-ip>:<port> \ --cluster-from <src-node-id> \ --cluster-to <dst-node-id> \ --cluster-slots 1000删除节点前必须先迁走其槽位,否则拒绝删除。性能优化配置# 内存maxmemory 2gbmaxmemory-policy allkeys-lruecho never > /sys/kernel/mm/transparent_hugepage/enabled # 关闭 THP# 持久化appendonly yesappendfsync everysec # 折中方案,最多丢 1 秒数据auto-aof-rewrite-min-size 64mb# 网络tcp-backlog 511tcp-keepalive 300timeout 300 # 空闲连接超时告警建议:内存使用率 > 80%、QPS 下降 > 50%、延迟 > 100ms、连接数 > 80% maxclients。以上内容覆盖了 Redis 监控运维的核心考察点,面试中回答时应遵循"指标→工具→排查→优化"的递进逻辑,展示系统性思维而非零散知识点。
服务端阅读 05月28日 09:36

Redis 的过期策略和内存淘汰机制是什么?如何选择合适的策略?

Redis 的过期策略和内存淘汰机制是两个不同层面的问题:过期策略决定「过期 key 何时被删」,内存淘汰策略决定「内存不够时删谁」。过期策略Redis 采用惰性删除 + 定期删除的组合策略,不使用定时删除。惰性删除:访问 key 时才检查是否过期。由 expireIfNeeded() 函数实现,所有读写命令执行前都会调用。优点是 CPU 友好,缺点是过期 key 若不被访问就永远占内存。定期删除:每秒执行约 10 次(受 hz 配置控制),每次随机抽取 20 个设置了过期时间的 key 检查,若过期则删除。若本轮过期 key 超过 25%,则继续抽样,直到低于 25% 或超时(25ms)。由 activeExpireCycle() 函数实现。为什么不单独用定时删除?创建大量定时器会严重消耗 CPU 资源,Redis 出于性能考虑弃用此方案。二者配合的效果:定期删除保证过期 key 不会长期滞留,惰性删除兜底处理定期删除遗漏的 key。内存淘汰策略当 Redis 内存使用达到 maxmemory 限制时,根据淘汰策略决定删除哪些 key。共 8 种策略:| 策略 | 淘汰范围 | 算法 | 适用场景 ||------|---------|------|----------|| noeviction | 不淘汰 | - | 数据不能丢失 || allkeys-lru | 全部 key | LRU | 纯缓存,热点集中 || allkeys-lfu | 全部 key | LFU | 纯缓存,访问频率差异大 || allkeys-random | 全部 key | 随机 | 所有 key 访问概率相近 || volatile-lru | 有过期时间的 key | LRU | 混合存储,保留持久数据 || volatile-lfu | 有过期时间的 key | LFU | 同上,优先淘汰低频临时数据 || volatile-random | 有过期时间的 key | 随机 | 临时数据随机淘汰 || volatile-ttl | 有过期时间的 key | TTL 最短 | 优先淘汰即将过期的 key |LRU 与 LFU 的实现Redis 使用近似 LRU,并非精确 LRU。每个 key 记录最后访问时间戳(24bit lru 字段),淘汰时随机采样 N 个 key(默认 5 个),删除其中最久未访问的。采样数越大越接近精确 LRU,但 CPU 开销也越大。LFU 在 Redis 4.0 引入,复用 lru 字段的高 16 位记录衰减时间、低 8 位记录访问计数器。计数器会随时间衰减,避免历史高频 key 永远不被淘汰。如何选择纯缓存场景(数据全可丢失):allkeys-lru(推荐)或 allkeys-lfu部分数据持久化(重要数据不设过期时间):volatile-lru 或 volatile-lfu数据绝对不能丢:noeviction所有 key 访问概率均匀:allkeys-random生产环境推荐优先考虑 allkeys-lru,绝大多数缓存场景都适用。若使用 Redis 4.0+ 且访问频率差异明显,allkeys-lfu 更精准。用 INFO memory 命令监控内存使用,关注 used_memory、used_memory_peak、maxmemory 等指标。追问Q:过期策略和内存淘汰策略的关系?过期策略处理的是「已过期的 key 何时删除」,是时间驱动的;内存淘汰策略处理的是「内存不足时删谁」,是空间驱动的。两者互补:即使过期策略遗漏了部分过期 key,内存淘汰策略也能在内存紧张时兜底清理。Q:为什么 Redis 的 LRU 是近似的?精确 LRU 需要维护全局链表,每次访问都要移动节点,O(1) 的 key 访问变成 O(n) 的链表操作。近似 LRU 随机采样 5 个 key 淘汰最旧的,牺牲少量精度换取 O(1) 的访问性能。实测表明近似 LRU 的命中率接近精确 LRU。Q:volatile 系列策略的一个潜在问题?如果没有 key 设置过期时间,volatile 策略等同于 noeviction,不会淘汰任何 key,可能导致内存满后写入全部失败。
服务端阅读 05月28日 09:35

Redis 主从复制、哨兵和集群模式有什么区别?高可用方案怎么选?

Redis 的主从复制、哨兵模式和集群模式是三种不同层次的高可用方案,它们解决的核心问题不同,选择时需要根据业务的数据规模、可用性要求和读写瓶颈来决定。主从复制:解决读瓶颈和数据备份主从复制是最基础的方案。一个 Redis 节点作为 Master 负责写操作,一个或多个 Slave 节点从 Master 同步数据,只提供读服务。同步过程分两个阶段:首次连接时 Master 执行 BGSAVE 生成 RDB 快照发送给 Slave(全量同步);之后的写命令通过 repl-backlog 缓冲区持续发送给 Slave(增量同步)。如果 Slave 断开时间过长,缓冲区被覆盖,就会再次触发全量同步。slaveof 192.168.1.100 6379主从复制的核心价值是读写分离——把读流量分散到多个 Slave 上,同时 Slave 作为数据副本提供冗余。但它的致命问题是 Master 挂了之后需要人工介入,把某个 Slave 手动提升为 Master,还要修改应用配置指向新地址,恢复时间完全取决于运维响应速度。适用场景:数据量不大、读多写少、能容忍短暂不可用的内部系统。比如配置中心、排行榜这类读远多于写的业务。哨兵模式:在主从基础上实现自动故障转移哨兵模式本质是主从复制 + 自动化运维。部署一组 Sentinel 进程(至少3个,奇数个),它们持续监控 Master 和 Slave 的状态,Master 故障时自动完成故障转移。故障转移的核心机制:主观下线与客观下线单个 Sentinel 检测到 Master 无响应(超过 down-after-milliseconds),标记为主观下线(SDOWN)。但这可能只是网络抖动,所以需要多个 Sentinel 互相确认——当超过 quorum 个数的 Sentinel 都认为 Master 下线时,才标记为客观下线(ODOWN),此时才会触发真正的故障转移。故障转移流程Sentinel 集群通过 Raft 协议选出一个 Leader Sentinel 执行转移Leader 从 Slave 中选出新 Master(优先判断复制偏移量最大、连接最稳定的节点)对新 Master 执行 SLAVEOF NO ONE,将其提升为主节点通知其他 Slave 复制新 Master更新 Sentinel 的监控配置port 26379sentinel monitor mymaster 192.168.1.100 6379 2sentinel down-after-milliseconds mymaster 30000sentinel failover-timeout mymaster 180000配置中 2 表示至少需要 2 个 Sentinel 同意才能判定客观下线。生产环境建议至少部署 3 个 Sentinel,跨机房分布,避免 Sentinel 自身成为单点。客户端需要使用 Sentinel 模式连接——先从 Sentinel 获取当前 Master 地址,再连接 Master。当故障转移完成后,Sentinel 会通知客户端切换到新 Master。局限性哨兵解决了自动故障转移,但写操作仍然只能走 Master 单点,存储容量也受限于单机内存。当数据量超过单机内存,或者写 QPS 成为瓶颈时,哨兵模式就不够用了。适用场景:数据量在单机内存范围内,但需要高可用、要求故障自动恢复的业务。大部分中小规模的生产环境用哨兵模式就足够了。集群模式:解决写瓶颈和存储瓶颈Redis Cluster 是真正的分布式方案,通过数据分片把数据分散到多个 Master 节点上,每个 Master 持有部分数据,同时可以有自己的 Slave 做副本。哈希槽分片机制Cluster 把整个键空间划分为 16384 个哈希槽,每个 Master 负责一部分槽。客户端写入一个 key 时,用 CRC16(key) % 16384 计算出槽编号,然后路由到负责该槽的节点。当集群需要扩容时,只需把一部分哈希槽从旧节点迁移到新节点,不需要停服。收缩时反向操作。请求重定向:MOVED 和 ASK客户端向错误节点发送请求时,该节点会返回重定向指令:MOVED:表示该槽已经永久迁移到新节点,客户端应更新本地缓存ASK:表示该槽正在迁移中(临时状态),客户端本次请求转发到目标节点,但不更新缓存cluster-enabled yescluster-config-file nodes.confcluster-node-timeout 5000创建集群:redis-cli --cluster create \ 192.168.1.1:7000 192.168.1.2:7001 192.168.1.3:7002 \ 192.168.1.4:7003 192.168.1.5:7004 192.168.1.6:7005 \ --cluster-replicas 1节点通信与故障检测Cluster 采用 Gossip 协议进行节点间通信,每个节点定期向其他节点发送 PING 消息交换集群状态。当某个 Master 被超过半数 Master 标记为故障(PFAIL → FAIL),其 Slave 会自动提升为新 Master。限制不支持跨槽的多键操作(MGET、MSET 等操作的 key 必须在同一槽,可用 hash tag {tag} 解决)不支持跨槽的 WATCH 事务KEYS、SCAN 等命令只能看到当前节点的数据最少需要 6 个节点(3 主 3 从)才能保证高可用适用场景:数据量超过单机内存、写 QPS 单机无法承载、需要在线扩缩容的大规模业务。三种方案的核心区别| 特性 | 主从复制 | 哨兵模式 | 集群模式 ||------|---------|---------|---------|| 故障转移 | 手动 | 自动(Sentinel 选举) | 自动(Gossip + 投票) || 写扩展 | 不支持 | 不支持 | 支持(数据分片) || 存储扩展 | 受限于单机 | 受限于单机 | 支持(水平扩容) || 最小部署 | 2 节点 | 2 Redis + 3 Sentinel | 6 节点(3主3从) || 运维复杂度 | 低 | 中 | 高 || 客户端要求 | 普通 | 支持 Sentinel 协议 | 支持 Cluster 协议 |面试中如何回答面试官问这个问题,核心考察的是你对 Redis 高可用演进路径的理解,而不只是背诵概念。建议从问题驱动的角度组织回答:第一步,先说清楚每种方案解决什么问题——主从解决读瓶颈,哨兵解决自动故障转移,集群解决写瓶颈和存储瓶颈。这三个是递进关系,不是并列选择。第二步,结合实际场景给出选型依据:数据量小 + 可容忍短时不可用 → 主从;数据量小 + 要求高可用 → 哨兵;数据量大或写 QPS 高 → 集群。第三步,补充生产经验。比如哨兵部署要跨机架、集群扩容时注意槽迁移对大 key 的影响、客户端重试策略要处理 MOVED/ASK 重定向等。这些细节能体现你的实战深度。
服务端阅读 05月28日 09:35

Redis 是什么?核心特点有哪些?

Redis 是什么?Redis(Remote Dictionary Server)是用 C 语言编写的开源高性能键值存储系统。它将数据全部驻留在内存中,通过单线程事件驱动模型处理请求,单机 QPS 可达 10 万+。与 Memcached 等纯缓存不同,Redis 提供持久化、事务、Lua 脚本、发布订阅等能力,既能做缓存,也能承担消息队列、会话存储、排行榜等业务角色。主要特点基于内存的高性能读写Redis 所有数据存储在内存中,内存访问延迟在纳秒级,远低于磁盘的毫秒级延迟。配合 I/O 多路复用(epoll/kqueue)和单线程事件循环,避免了线程切换和锁竞争的开销,这是 Redis 高吞吐的根本原因。Redis 6.0 引入了多线程 I/O,但命令执行仍是单线程——多线程只负责网络读写,执行命令本身仍串行执行,既保证了原子性,又提升了网络 I/O 瓶颈下的吞吐。丰富的数据结构Redis 远不止简单的 key-value,它原生支持 9 种数据类型:| 类型 | 典型场景 ||------|---------|| String | 缓存、计数器、分布式锁 || Hash | 对象存储(用户信息、商品属性) || List | 消息队列、最新消息排行 || Set | 去重、交集/并集运算(共同关注) || ZSet | 排行榜、延迟队列 || Bitmap | 签到、在线状态 || HyperLogLog | UV 统计(允许误差) || Geo | 附近的人、距离计算 || Stream | 消息队列(支持消费者组) |每种类型底层有对应的编码实现,Redis 会根据数据量自动选择编码以节省内存。例如 List 在元素少时使用 ziplist(紧凑列表),元素多时切换为 quicklist。双模式持久化Redis 提供两种持久化机制,可以单独使用也可以组合使用:RDB(快照):在指定时间间隔内将内存数据集写入磁盘的二进制文件。优点是文件紧凑、恢复速度快;缺点是两次快照之间的数据可能丢失。AOF(追加日志):将每个写命令追加到日志文件末尾。数据安全性更高,最多丢失 1 秒数据(everysec 策略);但日志文件体积更大,恢复速度较慢。生产环境通常两者同时开启:RDB 用于快速恢复,AOF 用于保证数据完整性。Redis 4.0+ 的混合持久化模式,在 AOF 重写时将当前数据以 RDB 格式写入,后续增量命令以 AOF 格式追加,兼顾了恢复速度和数据安全。原子性操作与事务Redis 单个命令是原子性的,要么执行成功要么不执行。对于需要多个命令原子执行的场景,Redis 提供了事务机制:MULTI # 开启事务SET key1 v1SET key2 v2EXEC # 提交事务MULTI 到 EXEC 之间的命令会按顺序串行执行,不会被其他客户端打断。但需要注意,Redis 事务不支持回滚——如果某条命令执行失败,其余命令仍会继续执行。这是 Redis 设计者有意为之,目的是保持简单高效。配合 WATCH 命令可以实现乐观锁:在事务执行前监控 key,若 key 被其他客户端修改,事务自动取消。主从复制与高可用Redis 通过主从复制实现读写分离和数据冗余:全量同步:从库首次连接主库时,主库生成 RDB 快照发送给从库增量同步:主库将新的写命令持续发送给从库(基于 replication offset 和 repl_backlog)Redis Sentinel 在主从基础上实现自动故障转移:监控主库状态,主库宕机时自动选举新主库并通知客户端切换。选举依据优先级、复制偏移量、run_id 三个维度排序。集群与水平扩展Redis Cluster 通过哈希槽(Hash Slot)实现数据分片,共 16384 个槽位分配到不同节点:# 集群中查看 key 所属槽位CLUSTER KEYSLOT mykey每个节点负责一部分槽位,客户端通过 MOVED 重定向找到目标节点。集群支持自动故障检测和转移,当某个主节点不可用时,其从节点自动升主。发布订阅与 StreamRedis 内置 Pub/Sub 模式,支持频道订阅和模式匹配订阅:SUBSCRIBE channel1 # 订阅频道PUBLISH channel1 "hello" # 发布消息Pub/Sub 的局限是消息不持久化,离线客户端无法收到历史消息。Redis 5.0 引入的 Stream 类型弥补了这一缺陷——它支持消息持久化、消费者组、消息确认(ACK),可以作为轻量级消息队列使用。Lua 脚本支持Redis 支持在服务端执行 Lua 脚本,脚本在执行期间不会被其他命令打断,适合需要原子性的复合操作:-- 限流脚本示例:每秒最多允许 N 次请求local key = KEYS[1]local limit = tonumber(ARGV[1])local count = redis.call("INCR", key)if count == 1 then redis.call("EXPIRE", key, 1)endif count > limit then return 0endreturn 1内存优化机制Redis 使用多种策略控制内存使用:共享对象池复用小整数(0-9999)、ziplist/listpack 紧凑编码节省小数据内存、惰性删除避免大 key 阻塞主线程。配合 maxmemory 配置和淘汰策略(如 allkeys-lru、volatile-lfu),可以在内存不足时自动回收低价值 key。面试追问方向Redis 为什么快? 内存存储 + I/O 多路复用 + 单线程避免锁竞争 + 高效数据结构编码。Redis 为什么早期用单线程? CPU 不是瓶颈,内存和网络才是;单线程避免上下文切换和锁开销,实现简单可靠。RDB 和 AOF 怎么选? 对数据完整性要求高选 AOF,对恢复速度要求高选 RDB,生产环境建议混合持久化同时开启。
服务端阅读 05月28日 09:34

Redis 有哪些数据类型?各自的底层实现和使用场景是什么?

Redis 提供了丰富的数据类型,面试中经常考察每种类型的底层编码、转换条件以及典型场景。下面从五大数据类型讲起,再覆盖后续新增的特殊类型。String:最基础的键值类型String 是 Redis 最简单的数据类型,可以存储字符串、整数、浮点数,最大 512MB。底层实现:Redis 没有直接使用 C 语言字符串,而是自己实现了 SDS(Simple Dynamic String)。SDS 在 C 字符串末尾 \0 的基础上增加了 len 和 alloc 字段:len 记录已用长度,alloc 记录分配总空间。这样做带来了三个好处——获取字符串长度从 O(N) 降为 O(1)、二进制安全(不依赖 \0 判断结尾)、空间预分配和惰性释放减少内存重分配次数。String 有三种编码:int(8字节以内整数)、embstr(44字节以内短字符串,一次分配内存)、raw(超过 44 字节,两次分配)。使用场景:缓存用户信息或配置、分布式锁(SETNX + 过期时间)、计数器(INCR 原子自增)、Session 共享。List:有序可重复的列表List 是一个按插入顺序排序的字符串元素集合,支持从两端推入和弹出。底层实现:Redis 3.2 之前用 ziplist(元素少时)或 linkedlist(元素多时)。3.2 之后统一改用 quicklist,它是一个由 ziplist 节点组成的双向链表,兼顾了 ziplist 的省内存和 linkedlist 的快速插入删除。Redis 7.0 进一步将 ziplist 替换为 listpack,解决了 ziplist 的级联更新问题。使用场景:消息队列(LPUSH + BRPOP 实现阻塞队列)、最新消息列表(如朋友圈时间轴)、栈(LPUSH + LPOP)和队列(LPUSH + RPOP)操作。Hash:字段-值映射Hash 是键值对的集合,适合存储对象属性,类似 Java 的 HashMap。底层实现:元素数量少且单个元素体积小时使用 listpack(原 ziplist),超过阈值切换为 hashtable。hashtable 采用链地址法解决哈希冲突,Redis 还实现了渐进式 rehash——维护 ht[0] 和 ht[1] 两个哈希表,rehash 期间每次增删改查操作都会顺带迁移一部分桶,避免一次性迁移造成阻塞。使用场景:存储对象(用户信息、商品详情,比 String+JSON 更节省内存,可以只读写单个字段)、购物车(用户ID为key,商品ID为field,数量为value)。Set:无序唯一集合Set 是字符串元素的无序集合,元素不重复。底层实现:当所有元素都是整数且数量不超过 512 时使用 intset(有序整数数组,内存紧凑),否则切换为 hashtable(value 存 null,只用 key)。使用场景:标签系统、共同好友/共同关注(SINTER 取交集)、抽奖(SRANDMEMBER 随机取)、去重(SADD 自动去重)。ZSet:有序唯一集合ZSet 在 Set 的基础上每个元素关联一个 score,按 score 排序,是 Redis 中最复杂的数据类型之一。底层实现:元素少且小时使用 listpack,否则使用 skiplist + hashtable 的组合。hashtable 提供 O(1) 的成员查找,skiplist 提供范围查询能力。跳表的层高通过随机算法确定(每层晋升概率 1/4),平均 O(logN) 查找。为什么不用红黑树?跳表实现更简单、范围查询更方便(只需要在底层链表上遍历)、插入删除只需修改相邻节点指针。使用场景:排行榜(游戏积分、热搜榜)、延时队列(score 存到期时间,ZRANGEBYSCORE 取到期任务)、带权重的消息队列。五大类型对比| 类型 | 底层编码 | 是否有序 | 是否可重复 | 核心操作复杂度 ||------|----------|----------|------------|----------------|| String | int/embstr/raw | - | - | O(1) || List | listpack/quicklist | 插入序 | 可重复 | 两端 O(1),中间 O(N) || Hash | listpack/hashtable | 无序 | field不可重复 | O(1) || Set | intset/hashtable | 无序 | 不可重复 | O(1) || ZSet | listpack/skiplist+hashtable | score有序 | 不可重复 | 查找O(1),范围O(logN+M) |Bitmap:位级操作Bitmap 不是独立的数据类型,而是 String 上的位操作扩展。底层实现:基于 String(SDS),每个 bit 对应一个偏移量。SETBIT 将指定偏移位设为 0 或 1,BITCOUNT 统计设为 1 的位数。使用场景:用户每日签到(一个用户一年只需 365 bit ≈ 46 字节)、统计连续签到天数、在线状态判断、布隆过滤器。HyperLogLog:基数估算HyperLogLog 用极小的内存估算集合基数(不重复元素数量),标准误差约 0.81%。底层实现:基于概率算法,固定占用 12KB 内存(16384 个桶,每个 6 bit),不会随元素增多而增长。使用场景:网站 UV 统计(百万级 UV 只需 12KB)、大屏数据去重计数。不需要精确值时优先选择,比 Set 存储节省几个数量级内存。Geo:地理位置Geo 用于存储地理位置信息并进行距离计算。底层实现:基于 ZSet,使用 GeoHash 编码将经纬度转为 52 位整数作为 score。GEOADD 本质是 ZADD,GEORADIUS 本质是 ZRANGEBYSCORE + 距离计算。使用场景:附近的人/店铺、距离计算、地理围栏。Stream:消息流Stream 是 Redis 5.0 新增的数据类型,专门为消息队列场景设计,可以看作轻量版 Kafka。底层实现:使用 radix tree(基数树)+ listpack 实现。每条消息有全局递增的 ID(时间戳-序号),支持消费组(Consumer Group),同一条消息可被不同消费组各消费一次。使用场景:消息队列(相比 List 的 BRPOP,Stream 支持消费组、消息确认、历史回溯,解决了 List 无法 ACK 和无法回溯的问题)、事件日志、实时数据管道。与 List 做消息队列的区别:List 不支持消费组,一条消息只能被一个消费者取走;Stream 支持消费组,多条消息可分发到组内不同消费者并行处理,且消费者断线后未确认的消息可以转交给其他消费者。面试高频追问:Redis 为什么用跳表不用红黑树实现 ZSet?——跳表实现简单、范围查询只需遍历底层链表、插入删除只需修改相邻指针,而红黑树的旋转操作更复杂且范围查询需要中序遍历。Hash 和 List 的编码转换阈值是多少?——Hash 在 field 数量超过 hash-max-ziplist-entries(默认128)或单个 field 超过 hash-max-ziplist-value(默认64字节)时从 listpack 转为 hashtable;List 的 quicklist 每个 node 的 ziplist 大小由 list-max-ziplist-size 控制。
服务端阅读 05月28日 09:33

Redis 性能优化有哪些策略?

Redis 性能优化是面试中的高频考点,也是生产环境中必须掌握的实战技能。本文从内存、网络、CPU、持久化、集群、监控、OS、客户端、架构九大维度系统梳理优化策略,并结合面试常见追问给出关键知识点。内存优化选择合适的数据结构是内存优化的第一步:用 Hash 存储对象字段,比多个 String 节省内存(底层 ziplist/listpack 编码)用 ZSet 做排行榜,避免 List 排序开销用 Bitmap 存布尔型标记位,空间仅为 Set 的 1/64用 HyperLogLog 做基数统计,固定 12KB 即可统计亿级去重控制键名长度:键名占用内存不可忽视,但不宜过度缩写。user:1001:profile 比 u:1001:pf 更可维护,而 user:profile:1001:detail:info 则过长。调整紧凑编码阈值:Hash、List 在元素少时自动使用 ziplist(Redis 7.0+ 为 listpack),通过 hash-max-ziplist-entries、hash-max-ziplist-value 调整触发阈值,在内存和性能间取平衡。设置过期时间:为临时数据设置 TTL,避免无用数据长期占内存。用 EXPIRE/EXPIREAT 管理,配合惰性删除 + 定期删除策略回收内存。淘汰策略选择:Redis 提供八种淘汰策略(Redis 4.0+),面试常考:allkeys-lru:通用场景首选,淘汰最久未用的 keyvolatile-lru:只淘汰设了过期时间的 key,适合缓存+持久共存allkeys-lfu(Redis 4.0+):淘汰访问频率最低的 key,热点数据友好noeviction:默认策略,内存满拒绝写入,数据不能丢的场景使用延迟删除(Lazy Free):Redis 4.0+ 引入异步删除,避免大 key 删除阻塞主线程:lazyfree-lazy-eviction yeslazyfree-lazy-expire yeslazyfree-lazy-server-del yesreplica-lazy-free yes用 UNLINK 替代 DEL 删除大 key,后台线程异步回收内存。内存碎片治理:监控 INFO memory 中 mem_fragmentation_ratio,超过 1.5 说明碎片严重。开启主动碎片整理:activedefrag yes网络优化Pipeline 批量执行:将多个命令打包一次发送,减少网络 RTT。适合批量写入、批量查询场景:# Pipeline 批量设置echo -e "SET key1 value1\nSET key2 value2\nSET key3 value3" | redis-cli --pipe注意 Pipeline 不是原子操作,中间命令失败不影响其他命令执行。连接池管理:客户端使用连接池复用连接,避免频繁 TCP 握手。池大小建议按 连接数 = (RTT × QPS) / 命令数 估算,一般 50-200 即可覆盖大多数场景。大 Key 拆分:大 Key 导致网络传输慢、阻塞主线程、影响同步。排查方式:redis-cli --bigkeys # 扫描各类型最大 keyredis-cli MEMORY USAGE key # 查看单个 key 内存占用拆分策略:将大 Hash 拆为多个小 Hash(按字段分桶),大 List 用 LRANGE 分段读取。禁用 THP:Transparent Huge Pages 会导致 fork 耗时剧增,影响 RDB/AOF 重写:echo never > /sys/kernel/mm/transparent_hugepage/enabledCPU 优化禁用 KEYS 命令:KEYS 会遍历所有 key,时间复杂度 O(N),生产环境必须禁用。用 SCAN 增量迭代替代:SCAN 0 MATCH user:* COUNT 100避免慢命令:SORT 大集合、SUNION/SINTER 大集合操作、LRANGE 0 -1 全量读取等都是常见性能杀手。核心原则:单次操作时间控制在毫秒级。Lua 脚本减少往返:Lua 在服务端原子执行,减少网络 RTT,适合复合原子操作:-- 原子性扣库存并返回余量local stock = tonumber(redis.call("GET", KEYS[1]))if stock and stock > 0 then redis.call("DECR", KEYS[1]) return stock - 1else return -1end注意 Lua 脚本执行期间 Redis 阻塞,脚本必须简短。Redis 6.0+ 多线程 IO:Redis 6.0 引入多线程处理网络读写,命令执行仍是单线程。开启方式:io-threads 4io-threads-do-reads yes适合网络 IO 成为瓶颈的场景(高并发小命令),CPU 密集型操作无法加速。持久化优化RDB 调优:调整保存频率,低峰期触发;关闭 RDB 压缩(rdbcompression no)可降低 CPU 开销,但文件会变大。save "" 可完全禁用 RDB。AOF 调优:appendfsync everysec 是性能与安全的平衡点,最多丢 1 秒数据调整 auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 控制重写触发低峰期手动 BGREWRITEAOF 减少影响混合持久化(Redis 4.0+):aof-use-rdb-preamble yes,AOF 重写时前半段写 RDB 格式(快),后半段写 AOF 增量(全),兼顾恢复速度和数据安全。纯缓存场景关闭持久化:如果 Redis 只做缓存、数据可从 DB 重建,关闭 RDB 和 AOF 可大幅提升性能。集群优化数据分片与避免倾斜:Redis Cluster 按 hash slot 分片(16384 个),用 CLUSTER SLOT 查看分布。避免倾斜的方法:合理设计 key 的 hash tag({tag}),热点 key 拆分为多个 key 分散到不同 slot。读写分离:从节点分担读流量,注意从节点默认拒绝写操作。复制延迟可能导致读到旧数据,强一致性场景慎用。故障转移:哨兵模式(Sentinel)适合主从架构的自动故障转移;Redis Cluster 自带故障检测和转移能力。面试常问两者的区别:Sentinel 是独立进程监控主从,Cluster 是去中心化的分片+高可用方案。监控与调优慢查询日志:CONFIG SET slowlog-log-slower-than 10000 # 阈值 10msCONFIG SET slowlog-max-len 128SLOWLOG GET 10 # 查看最近 10 条慢查询INFO 命令关键指标:INFO memory # used_memory、fragmentation_ratioINFO commandstats # 命令调用次数和耗时INFO replication # 主从复制状态和延迟INFO stats # keyspace_hits/misses 算命中率redis-benchmark 压测:redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 10000 -t set,get操作系统优化文件描述符:ulimit -n 65535# 或永久配置 /etc/security/limits.confTCP 参数:echo 3 > /proc/sys/net/ipv4/tcp_fastopen# 调整 TCP 缓冲区sysctl -w net.core.somaxconn=65535关闭 THP:已在网络优化中提及,这里再次强调,这是 Redis 生产部署的必要步骤。绑定 CPU:taskset 将 Redis 绑定到特定 CPU 核,减少上下文切换,对延迟敏感场景有效。客户端优化客户端选型:Jedis:同步阻塞,简单场景够用Lettuce:基于 Netty 的异步非阻塞,Spring Boot 2.x 默认Redisson:分布式锁、限流器等高级功能开箱即用多级缓存:本地缓存(Caffeine/Guava Cache)+ Redis 二级缓存,热点数据本地缓存 1-5 秒,减少 Redis 访问量。避免 N+1 查询:用 MGET/Pipeline 批量查询,用 Hash 结构化存储减少查询次数。架构优化Redis Proxy:Twemproxy、Predixy 等代理层实现分片和连接池管理,对客户端透明。缺点是多一跳网络开销。高可用方案选型:主从 + Sentinel:适合数据量不大、读多写少Redis Cluster:适合数据量大、需要水平扩展两者不兼容,架构设计时需提前选型多实例部署:单机多实例可充分利用多核 CPU,每个实例绑定不同核,独立持久化互不影响。面试答题思路面试官问 Redis 性能优化,建议按优先级回答:最优先:禁用 KEYS、大 Key 拆分、Pipeline、设置合理 TTL 和淘汰策略持久化:AOF everysec、混合持久化、纯缓存可关闭架构层:读写分离、Cluster 分片、多级缓存运维层:慢查询监控、内存碎片治理、OS 参数调优每个点说出原理和具体操作,面试官追问时能展开即可。不要泛泛而谈"多维度优化",要落地到具体配置项和命令。
服务端阅读 05月28日 09:33

Redis 的 RDB 和 AOF 持久化有什么区别?如何选择?

Redis 提供两种持久化机制将内存数据写入磁盘:RDB(快照)和 AOF(追加日志)。理解两者的原理和取舍是后端面试的高频考点,也是生产环境配置的基础。RDB 持久化:定时快照RDB 在指定时间间隔内对数据集生成时间点快照,写入压缩的二进制文件 dump.rdb。触发方式:手动触发:执行 SAVE(阻塞主进程)或 BGSAVE(fork 子进程后台执行)自动触发:配置 save <seconds> <changes> 条件满足时自动执行 BGSAVEshutdown 时若开启 RDB 且无 AOF,默认执行 BGSAVE核心原理 — COW(Copy-On-Write):BGSAVE 时 Redis 通过 fork() 创建子进程,子进程共享父进程的内存页。当主进程收到写请求,操作系统会将待修改的内存页复制一份,子进程继续在原页面写入 RDB 文件。这就是为什么 RDB 对主进程性能影响小——只有真正被修改的页才会产生额外内存开销。优点:文件紧凑:二进制压缩格式,体积远小于 AOF,适合备份和传输恢复速度快:直接加载二进制文件,比 AOF 重放命令快一个数量级对主进程影响小:由子进程执行,COW 机制保证主进程正常处理请求适合冷备份:单文件结构,方便定时拷贝到远程存储缺点:数据丢失风险高:两次快照之间的数据变更全部丢失,最坏情况丢失数分钟数据fork 耗时:数据量大时 fork 本身可能阻塞主进程(通常与数据集大小成正比)无法实时持久化:基于时间间隔,做不到每秒甚至每次写的持久化关键配置:save 900 1 # 900秒内至少1个key变化save 300 10 # 300秒内至少10个key变化save 60 10000 # 60秒内至少10000个key变化rdbcompression yes # 压缩RDB文件rdbchecksum yes # 文件校验stop-writes-on-bgsave-error yes # BGSAVE失败时拒绝写入AOF 持久化:追加日志AOF 记录每一条写操作命令,以文本格式追加到 appendonly.aof 文件末尾。Redis 重启时逐条重放命令恢复数据。写入流程:命令追加(append) → 写入缓冲区(aof_buf) → 同步到磁盘(fsync)同步策略(appendfsync 配置项):always:每次写操作都 fsync,最多丢一条命令,但性能最差everysec:每秒 fsync 一次,最多丢 1 秒数据,推荐生产配置no:由操作系统决定何时 fsync,性能最好但丢失风险不可控AOF 重写机制:AOF 文件会持续膨胀。Redis 通过 BGREWRITEAOF 命令在后台重写:fork 子进程遍历当前数据库状态,用最少命令重新生成 AOF 文件。例如对同一 key 执行 100 次 SET,重写后只保留最后一条。重写期间新的写命令同时写入旧 AOF 和重写缓冲区,重写完成后将缓冲区追加到新文件。优点:数据安全性高:everysec 策略下最多丢失 1 秒数据可读可修复:文本格式,可直接查看;误操作后可手动编辑删除错误命令自动重写压缩:配置阈值触发重写,控制文件体积缺点:文件体积大:同等数据量下 AOF 文件通常比 RDB 大数倍恢复速度慢:逐条重放命令,大规模数据恢复耗时显著性能开销大:频繁的磁盘 I/O,always 策略下吞吐量明显下降关键配置:appendonly yes # 开启AOFappendfsync everysec # 同步策略auto-aof-rewrite-percentage 100 # AOF文件大小增长100%时触发重写auto-aof-rewrite-min-size 64mb # AOF文件重写的最小大小aof-load-truncated yes # 忽略末尾不完整的AOF文件RDB + AOF 混合持久化Redis 4.0 引入混合持久化,兼顾两者优势:AOF 重写时将 RDB 格式的全量数据写入 AOF 文件开头,后续增量命令以 AOF 格式追加。效果:恢复时先快速加载 RDB 部分(快),再重放增量 AOF 命令(完整),数据安全性与恢复速度兼得。aof-use-rdb-preamble yes # 开启混合持久化(需同时开启AOF)恢复优先级:当 RDB 和 AOF 文件同时存在时,Redis 优先加载 AOF,因为 AOF 的数据完整性更高。面试怎么答:对比速记表| 维度 | RDB | AOF ||------|-----|-----|| 原理 | 定时快照(二进制) | 追加写命令(文本日志) || 数据安全性 | 可能丢失数分钟数据 | 最多丢 1 秒(everysec) || 恢复速度 | 快 | 慢 || 文件体积 | 小 | 大 || 性能影响 | 小(COW) | 较大(频繁 I/O) || 适用场景 | 冷备份/灾难恢复 | 实时持久化/数据安全优先 |生产环境选择建议数据安全优先(金融、支付):AOF + appendfsync everysec,或混合持久化性能优先、可容忍少量丢失(缓存场景):仅 RDB,配置合理的 save 间隔两者兼顾(通用生产环境):RDB + AOF 混合持久化,这是 Redis 4.0+ 的推荐方案纯缓存无需持久化:关闭 RDB 和 AOF,重启后从数据源重新加载实际生产中,大多数场景采用混合持久化。关闭 RDB 的 save 配置(设为空字符串)可以避免自动触发快照,仅保留 AOF 的实时性,同时定期手动 BGSAVE 做冷备。
服务端阅读 05月28日 07:19

Redis 事务、Lua 脚本和分布式锁的实现原理和使用场景是什么?

Redis 事务、Lua 脚本和分布式锁是 Redis 面试中出现频率最高的三个高级特性,很多候选人只能说出命令用法,却讲不清背后的原理和边界,面试官一追问就卡壳。下面逐一拆解。Redis 事务的原理与局限Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 四个命令协作完成。MULTI 开启事务后,后续命令进入队列而非立即执行;EXEC 一次性提交队列中的所有命令;DISCARD 放弃事务;WATCH 实现乐观锁,监控 key 是否在事务提交前被修改。WATCH balanceMULTIDECRBY balance 100INCRBY expense 100EXEC如果 WATCH 监控的 key 在 EXEC 之前被其他客户端修改,整个事务会被丢弃,返回 nil。事务的核心特点:命令按顺序执行,不会被其他客户端插入,这是隔离性的保证。不支持回滚。如果队列中某条命令执行失败(比如对字符串执行 LPUSH),其余命令照常执行。Redis 官方的设计哲学是:命令失败属于编程错误,应在开发阶段发现,不值得为此牺牲性能。无法使用中间结果。事务中的命令不能引用前一条命令的返回值,这极大限制了事务的表达能力。这些局限正是 Lua 脚本存在的理由。追问:Redis 事务为什么不支持回滚?Redis 作者 antirez 的原话是:回滚需要保存命令执行前的状态,这会引入与 AOF 持久化类似的复杂度,而命令失败本质是 bug,不应该出现在生产环境。所以 Redis 选择不支持回滚,换来更简单、更快的实现。Lua 脚本为什么能替代事务Lua 脚本在 Redis 服务端以原子方式执行,执行期间不会处理其他客户端的命令。和事务相比,Lua 脚本的核心优势在于可以读取中间结果并做条件判断。EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue先用 SCRIPT LOAD 获取脚本 SHA1 校验和,后续用 EVALSHA 执行,避免每次传输完整脚本:SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"# 返回 sha1sumEVALSHA <sha> 1 mykey myvalue几个典型的 Lua 脚本应用场景:原子性 CAS 操作——只有当值等于预期时才更新:local current = redis.call('GET', KEYS[1])if current == ARGV[1] then redis.call('SET', KEYS[1], ARGV[2]) return 1else return 0end滑动窗口限流器——一次执行完成过期清理、计数检查、记录添加三个步骤:local key = KEYS[1]local limit = tonumber(ARGV[1])local window = tonumber(ARGV[2])redis.call('ZREMRANGEBYSCORE', key, '-inf', window)local count = redis.call('ZCARD', key)if count < limit then redis.call('ZADD', key, window, ARGV[3]) redis.call('EXPIRE', key, math.ceil((window - tonumber(ARGV[4])) / 1000)) return 1else return 0endLua 脚本需要注意三点:执行时间不能太长,否则阻塞整个 Redis 实例(默认 5 秒超时);不能使用随机函数(如 math.random),否则主从复制结果不一致;不能执行阻塞命令(如 BLPOP)。追问:Lua 脚本出错会怎样?Lua 脚本运行时出错会立即停止,但已执行的 Redis 命令不会被撤销——这点和事务一致,都不满足严格意义上的原子性。从外部看,脚本的执行是不可分割的;从内部看,部分成功部分失败是可能的。分布式锁的三种实现与踩坑分布式锁要解决的核心问题:在多进程、多机器环境下,保证同一时刻只有一个客户端能操作共享资源。SETNX + EXPIRE 的问题最早的做法是先 SETNX 获取锁,再 EXPIRE 设置过期时间。这两步不是原子操作——如果 SETNX 成功后客户端崩溃,锁永远不释放,造成死锁。SET NX EX(推荐的基础方案)Redis 2.6.12 起 SET 命令支持 NX 和 EX 参数,一条命令完成加锁和设置过期时间:public boolean tryLock(String key, String value, int expireSeconds) { String result = jedis.set(key, value, "NX", "EX", expireSeconds); return "OK".equals(result);}释放锁必须用 Lua 脚本保证原子性——先判断值是否为自己持有,再删除:if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1])else return 0end为什么不能直接 DEL?因为锁可能已经过期并被其他客户端获取,直接 DEL 会删掉别人的锁。追问:主从切换导致锁丢失怎么办?考虑这个时序:客户端 A 获取锁 → 主节点写入成功但尚未同步到从节点 → 主节点宕机 → 从节点升为主节点 → 客户端 B 获取同一把锁 → 两个客户端同时持有锁。Redlock 算法就是为解决这个问题设计的。Redlock 算法Redlock 向 N 个(通常 5 个)独立的 Redis 实例获取锁,只要在大多数实例(≥3)上成功,且总耗时未超过锁的有效期,就认为加锁成功。这依赖的是时钟同步和多数派决策,不依赖主从复制。Redlock 的争议:Martin Kleppmann 在《How to do distributed locking》一文中指出 Redlock 依赖系统时钟,当时钟跳变时可能出错,建议使用 fencing token 方案。antirez 专门写了长文反驳。实际工程中,大多数团队选择接受 Redlock 的概率性安全保证,或直接使用单节点锁 + 幂等设计来规避风险。Redisson 的工程级实现Redisson 是 Java 生态最成熟的 Redis 分布式锁实现,提供了可重入锁、公平锁、读写锁、联锁等多种锁类型。核心设计:看门狗机制:默认锁过期时间 30 秒,Redisson 会启动一个后台线程,每 10 秒(过期时间的 1/3)自动续期,直到显式释放或客户端宕机。这解决了长任务执行期间锁过期被其他客户端获取的问题。RLock lock = redisson.getLock("myLock");try { if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { // 执行业务逻辑 }} finally { lock.unlock();}可重入性:Redisson 使用 Hash 结构存储锁信息,field 是客户端 ID + 线程 ID,value 是重入次数,加锁时重入次数 +1,解锁时 -1,减到 0 才真正释放。整个逻辑用 Lua 脚本保证原子性。事务、Lua 脚本与分布式锁的选型| 维度 | 事务 | Lua 脚本 | 分布式锁 ||------|------|----------|---------|| 原子性 | 命令不可插入,但不支持回滚 | 同事务 | 依赖实现方式 || 条件判断 | 不支持 | 完全支持 | 通过 Lua 脚本支持 || 网络开销 | 多次 RTT(除非 pipeline) | 一次 RTT | 多次 RTT || 典型场景 | 简单批量操作 | CAS、限流、复杂业务逻辑 | 跨进程互斥 |选择原则:事务适合不需要中间结果的简单批量操作;Lua 脚本适合需要条件判断、依赖中间结果的场景;分布式锁解决的是跨进程互斥问题,本质上依赖 Lua 脚本保证操作的原子性。面试中容易被追问的几个点Redis 事务和 MySQL 事务有什么区别? Redis 事务不支持回滚,没有隔离级别,不保证持久性——它只是批量执行命令的机制,和 ACID 事务有本质区别。Lua 脚本执行期间 Redis 宕机怎么办? 如果开启了 AOF 持久化,已执行的命令会被记录,重启后重放。如果没有持久化,数据丢失。关键点是 Lua 脚本的"原子性"只保证执行期间不被打断,不保证持久性和严格的原子性。分布式锁的过期时间怎么设置? 太短容易导致任务未完成锁就被释放,太长会导致客户端宕机后其他客户端等待过久。Redisson 的看门狗机制是最佳实践,动态续期比固定过期时间更可靠。
服务端阅读 05月28日 07:17

Redis 与 MySQL、MongoDB、Memcached 有什么区别?如何选择?

Redis、MySQL、MongoDB、Memcached 是后端开发中最常用的四种数据存储方案,面试中经常被放在一起考察。它们的设计目标完全不同,理解本质差异才能做出正确的技术选型。核心区别:一张表看懂| 维度 | Redis | MySQL | MongoDB | Memcached ||------|-------|-------|---------|-----------|| 存储介质 | 内存为主,可持久化 | 磁盘为主 | 磁盘(内存映射) | 纯内存 || 数据模型 | Key-Value + 多种数据结构 | 关系型表 | 文档型(BSON) | Key-Value(仅String) || 事务支持 | 有限事务(MULTI/EXEC) | 完整ACID | 4.0起支持多文档事务 | 无 || 持久化 | RDB + AOF | 天然持久化 | 天然持久化 | 无,宕机即丢 || 查询能力 | 按Key操作,有限范围查询 | SQL,复杂关联聚合 | MQL,支持索引和聚合 | 仅GET/SET || 单机QPS | 10万+ | 数千~数万 | 数万 | 10万+ || 一致性模型 | 最终一致性 | 强一致性 | 可配置(最终/强) | 无一致性保证 || 水平扩展 | Cluster分片 | 分库分表(复杂) | 原生分片 | 客户端一致性Hash |Redis vs MySQL:缓存与持久化的抉择本质区别在于存储介质和一致性模型。Redis 数据驻留内存,读写延迟在亚毫秒级,但默认不保证数据持久化——即使开启 AOF,也存在最多 1 秒的数据丢失窗口。MySQL 数据落盘,通过 redo log 和 doublewrite 机制保证数据安全,但读写延迟在毫秒到十毫秒级。面试中常问的一个问题:Redis 能否替代 MySQL?答案是不能。两者的定位完全不同:Redis 适合做缓存层和实时数据层,数据可以丢失或从源头重建MySQL 适合做持久化存储层,数据不能丢失且需要事务保证生产中的经典架构是 Redis + MySQL 组合:读请求先查 Redis,命中则直接返回;未命中则查 MySQL,结果回写 Redis。但这里有几个关键问题需要注意:双写一致性:先更新 MySQL 再删缓存,存在短暂不一致窗口。对一致性要求高的场景,可用 Canal 监听 binlog 同步更新 Redis缓存穿透:查询不存在的数据,绕过缓存直击数据库。用布隆过滤器或缓存空值解决缓存击穿:热点 Key 过期瞬间大量请求涌入数据库。用互斥锁或永不过期+异步刷新缓存雪崩:大批 Key 同时过期。用随机过期时间打散Redis vs MongoDB:两种 NoSQL 的不同思路Redis 和 MongoDB 虽然都属于 NoSQL,但设计哲学完全不同。Redis 追求极致性能,数据全在内存中,数据结构精心设计,每种操作的时间复杂度都有明确保证。它更像一个高性能的数据结构服务器。MongoDB 追求灵活性,文档模型允许 schema 自由变化,嵌套文档减少了关联查询的需要。它更像一个增强版的 MySQL。关键区别:数据量:Redis 受内存限制,通常存热点数据;MongoDB 可存储 TB 级数据查询复杂度:Redis 只支持基于 Key 的操作;MongoDB 支持条件查询、聚合管道适用场景:Redis 做缓存/计数器/排行榜/分布式锁;MongoDB 做内容管理/日志/IoT 数据/用户画像Redis vs Memcached:缓存之争的终局这组对比面试频率极高。核心结论:新项目直接选 Redis,没有理由选 Memcached。Memcached 的仅存优势:多线程架构,在 value 超过 100KB 时吞吐量更高更简单的部署,适合纯缓存场景Redis 全面碾压的点:支持丰富数据结构(Memcached 只有 String)支持持久化(Memcached 宕机数据全丢)支持主从复制和集群(Memcached 靠客户端分片)支持发布订阅、Lua 脚本、Stream(Memcached 无)单线程模型反而简化了并发控制,小数据性能更优Memcached 在 2015 年之前还有市场份额,现在基本已被 Redis 完全取代。面试中回答"选 Redis"即可,但要说清楚为什么。技术选型:根据场景做决策选型不是二选一,而是根据业务特征匹配最合适的工具。选 Redis 的场景:需要亚毫秒级响应、数据可以容忍短暂丢失、操作模式简单(按 Key 读写)。典型用例:缓存、Session、排行榜、计数器、分布式锁、限流器。选 MySQL 的场景:数据必须持久化且保证一致性、需要复杂关联查询和事务、业务模型稳定。典型用例:用户系统、订单系统、支付系统。选 MongoDB 的场景:数据结构频繁变化、单文档较大且需要灵活查询、需要水平扩展。典型用例:CMS、日志分析、IoT、用户画像。选 Memcached 的场景:基本没有了。除非维护旧系统,否则没有理由新项目选 Memcached。生产架构中的组合模式实际项目中几乎不会只用一种存储,常见的组合方案:Redis + MySQL(最经典):Redis 缓存热点数据,MySQL 持久化存储。注意做好双写一致性、缓存穿透/击穿/雪崩的防护。Redis + MySQL + MongoDB:Redis 做缓存,MySQL 存核心业务数据,MongoDB 存非结构化数据(日志、配置、内容)。Redis + MySQL + Elasticsearch:Redis 缓存,MySQL 存储,ES 负责全文检索和复杂搜索。无论哪种组合,核心原则是:每种存储只做它最擅长的事,不要让 MySQL 做缓存,也不要让 Redis 做持久化。追问:缓存与数据库的一致性如何保证?这是上述选型之后必然的追问,也是面试高频考点。先更新数据库,再删缓存 是最常用的策略。为什么是删缓存而不是更新缓存?因为更新可能涉及复杂计算,且并发场景下容易产生脏数据。一致性保证的三个层级:弱一致性:先更新DB再删缓存,容忍短暂不一致,适合大多数业务最终一致性:通过消息队列或 Canal 监听 binlog 异步更新缓存,保证最终一致强一致性:读写都走数据库,或使用分布式锁,牺牲性能换一致性,仅金融等场景使用面试时回答到第二层即可,重点是说清楚各方案的 trade-off。