Redis 要点

Redis 要点
  1. 持久化
RDBAOF
定期对内存数据生成快照(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 脚本:将复杂逻辑封装成一个原子操作。

  1. 事务(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 被其他客户端修改,事务失败
  2. 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算法 + 自动续期集群环境下的库存扣减、订单创建

案例

  1. 分布式锁实现(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页数据

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP