Redis 要点
Redis 要点
- 持久化
| RDB | AOF |
|---|---|
定期对内存数据生成快照(Snapshot),保存为 .rdb 文件 | 记录所有写操作命令,以日志形式追加到文件。 |
| save 900 1 # 900秒内有至少1次修改,触发保存 save 300 10 # 300秒内有至少10次修改,触发保存 save 60 10000 # 60秒内有至少10000次修改,触发保存 | appendonly **yes # 启用****AOF appendfsync everysec # 每秒同步一次(平衡性能与安全) # 其他选项:always(每次写都同步,安全但慢)、no(由系统决定) |
| 文件紧凑(适合备份/恢复),恢复速度快。 | 数据丢失风险低(最多丢1秒数据)。 |
| 可能丢失最后一次快照后的数据(如配置为每5分钟保存一次,服务器突然宕机,会丢失最近5分钟的数据)。 | 文件体积大,恢复速度慢。 |
- 高安全性:同时开启 RDB + AOF(重启时优先用 AOF 恢复)。
- 高性能:仅用 RDB。
2.高并发与性能优化
当大量用户同时访问 Redis 时,如何让它“又快又稳”?关键在于减少网络通信和资源消耗。
专业技巧
1.管道(Pipeline)
-
问题:客户端每次执行命令都要等待 Redis 返回结果(网络延迟高)。
-
解决:将多个命令打包一次性发送,减少网络往返次数。
-
▼python复制代码
pipe = r.pipeline() pipe.set('key1', 'value1') pipe.get('key1') pipe.execute() # 一次性发送所有命令 -
-
2.连接池(Connection Pool)
-
-
问题:频繁创建/销毁连接消耗资源。
-
解决:预先建立连接池,重复利用连接。
-
Java,使用 Jedis 库
-
▼java复制代码
JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(100); // 最大连接数 JedisPool pool = new JedisPool(config, "localhost"); try (Jedis jedis = pool.getResource()) { jedis.set("foo", "bar"); }
3.事务与 Lua 脚本
事务:保证一组命令的原子性(要么全部成功,要么全部失败)。
Lua 脚本:将复杂逻辑封装成一个原子操作。
-
-
-
事务(MULTI/EXEC)
基本用法:
▼bash复制代码MULTI # 开启事务 SET key1 "A" INCR key2 EXEC # 提交事务(原子执行)关键特性:
-
不支持回滚:Redis 事务没有回滚机制,语法错误会导致整个事务失败,运行时错误(如对字符串执行 INCR)不会影响其他命令。
-
乐观锁:结合WATCH
监控键值变化,实现 CAS(Check And Set)。
▼bash复制代码WATCH balance # 监控 balance 键 MULTI DECRBY balance 100 INCRBY savings 100 EXEC # 如果 balance 被其他客户端修改,事务失败
-
-
Lua 脚本
优势:
-
原子性:脚本执行期间不会被其他命令打断。
-
减少网络开销:复杂逻辑在服务端一次性执行。
-
性能优化:脚本在服务端执行,减少网络往返次数。通过
EVALSHA复用已缓存的脚本(SHA1摘要),降低带宽消耗
示例(实现库存扣减):
▼lua复制代码-- KEYS[1]=库存键,ARGV[1]=扣减数量 local stock = tonumber(redis.call('GET', KEYS[1])) if stock >= tonumber(ARGV[1]) then redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 -- 成功 else return 0 -- 库存不足 end调用方式(命令行):
▼bash复制代码EVAL "脚本内容" 1 inventory 10 # 参数:1个键(inventory),扣减10 -
| 简单命令组(无依赖) | 事务(MULTI/EXEC) | 实现简单,无需处理脚本缓存 |
|---|---|---|
| 跨键操作/复杂逻辑 | Lua脚本 | 保证原子性,支持条件判断(如:先查后改) |
| 高并发抢购类场景 | Lua脚本 + 管道 | 减少网络延迟,避免竞态条件(如:红包雨设计) |
4:高并发经典问题(实战解决方案)
1. 缓存穿透
问题描述: 恶意请求查询数据库中不存在的数据,导致请求直接穿透缓存层直达数据库。当高并发发生时,可能压垮数据库。
| 布隆过滤器 | 使用位数组存储所有可能存在的key哈希值 |
|---|---|
| 空值缓存 | 对不存在的数据缓存NULL值(设置短TTL) |
bloom先行存储了可能的key(比如1,2),当查询key=3这个没有的数据的时候,本该绕过缓存直达数据库的,这时候通过布隆过滤器可以过滤掉这些空查询
如果还是绕开了空值缓存,就提前对不存在的数据设置为NULL。这样就会返回NULL 值(但是要注意TTL应该远远短于正常的数据,几秒钟到几分钟即可,过长会导致缓存空间浪费,过短则无法有效缓解穿透压力)
生产建议: 布隆过滤器(RedisBloom模块)+ 空值缓存组合使用,拦截99%的穿透攻击
2.缓存击穿
描述:热点key失效,导致海量数据到达数据库层,造成数据库压力,秒杀、爆款抢购。
| 互斥锁(Mutex) | 使用Redis的SETNX实现分布式锁 | 保证数据强一致性 | 增加系统复杂度,锁等待影响吞吐量 |
|---|---|---|---|
| 逻辑过期时间 | 缓存永不过期,业务代码维护逻辑过期时间 | 避免锁竞争,性能优异 | 需要维护版本号,可能读到旧数据 |
(逻辑过期:不依赖 Redis 的自动过期机制(设置key的存活时间,到期直接删除),而是在缓存数据的 Value 中嵌入一个过期时间字段(如时间戳)。当请求访问时,先检查该时间字段是否过期,若过期则触发异步更新,但仍返回旧数据给用户。这样的可用性,使得即使key过期,缓存中仍然保存了旧值,可以返回给用户,适合高并发场景。
物理过期:edis 的 EXPIRE 命令直接设置 Key 的存活时间,到期后 Key 自动删除。此时若大量请求涌入,会直接穿透到数据库)
建议:互斥锁(强一致性)+热点数据永不过期(逻辑过期处理)
3.缓存雪崩
大量数据同时过期、失效,大量key同时过期或Redis集群宕机这时候查询,请求全部到达数据库
解决:
| 随机时段过期 | 基础TTL + 随机数(如3600±600秒) | 实现简单,有效分散过期时间 | 需要根据业务调整随机时间窗口 |
|---|---|---|---|
| 分层缓存架构 | 本地缓存(Caffeine)+ Redis分级缓存 | 提升系统可用性级别 | 架构复杂度高,数据一致性维护成本增加 |
| 熔断降级 | 使用Hystrix等工具进行流量控制 | 保护数据库不被压垮 | 可能影响正常用户体验 |
| 集群高可用 | Redis Sentinel/Cluster部署 | 提升系统容灾能力 | 运维复杂度增加,硬件成本提高 |
解决方法;随机时段过期+分层缓存架构,分散缓存失效时间
总结对比表
| 问题类型 | 核心矛盾 | 关键技术 | 适用场景 |
|---|---|---|---|
| 缓存穿透 | 无效Key攻击 | 布隆过滤器 + 空值缓存 | 高频查询不存在数据(如恶意爬虫) |
| 缓存击穿 | 热点Key失效 | 互斥锁 + 逻辑过期 | 秒杀商品、爆款文章访问 |
| 缓存雪崩 | 大规模Key同时失效 | 随机TTL + 多级缓存 | 大促期间缓存集中过期 |
| 分布式锁 | 多节点资源竞争 | Redlock算法 + 自动续期 | 集群环境下的库存扣减、订单创建 |
案例
- 分布式锁实现(Jedis原生版本)
▼Java复制代码public boolean lock(String key, String value, int expireSec) { Jedis jedis = jedisPool.getResource(); try { return "OK".equals(jedis.set(key, value, "NX", "PX", expireSec*1000)); } finally { jedis.close(); } } public boolean unlock(String key, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, 1, key, value); return result.equals(1L); }
2.缓存穿透防护
▼Java复制代码public User getUser(String userId) { String key = "user:" + userId; String json = jedis.get(key); if (json != null) { if ("".equals(json)) { // 空值标记 return null; } return JSON.parseObject(json, User.class); } User user = userDao.findById(userId); if (user == null) { jedis.setex(key, 60, ""); // 缓存空值60秒 return null; } jedis.setex(key, 3600, JSON.toJSONString(user)); return user; }
-
空值缓存:数据库不存在的键也缓存空字符串,避免反复查询数据库
-
过期时间:
- 空值设置短过期(如60秒),防止长期占用内存
- 正常数据设置长过期(如3600秒)
-
更新同步:当数据新增时需删除对应的空值缓存
Q1:为什么分布式锁需要设置过期时间?A1:防止持有锁的客户端崩溃后锁无法释放,导致死锁
Q2:Lua脚本比多命令操作的优势是什么? A2:Redis单线程执行Lua脚本,保证多个命令的原子性
Q3:ZSET如何实现分页查询? A3:使用
zrevrange指定偏移量,如zrevrange leaderboard 10 19获取第2页数据
