Skip to content

这是我学习 Redis 时整理的入门笔记。Redis 可能是被用得最多、也最容易被用错的中间件——十个项目里有九个拿它当缓存,但缓存穿透、缓存击穿、缓存雪崩、缓存和数据库不一致,几乎每个项目都踩过。这篇先把 Redis 的核心概念、数据结构、持久化机制和上手流程过一遍,代码按 Spring Boot 落地。


一、为什么需要 Redis

先想清楚"解决什么问题"。数据库(MySQL)的数据存在磁盘上,读写都要经过磁盘 IO,热点数据的并发一高就扛不住。Redis 把所有数据放在内存里,单线程处理请求,读写都是微秒级,比磁盘快几个数量级。它解决的核心问题:

  1. 缓存:把"频繁读、不怎么变"的热点数据放进 Redis,扛住大部分读流量,数据库只承受穿透到它那一层的请求。
  2. 分布式锁:多台服务器抢同一份资源(比如扣库存)时,需要一把"全局的锁",Redis 的 SET NX EX 是最轻量的实现。
  3. 计数器/限流INCR 原子自增,秒杀库存扣减、接口限流、点赞数都能做。
  4. 排行榜/实时统计:ZSet 有序集合天然支持按分数排序,排行榜、TopN 就是它的主场。
  5. 临时数据存储:验证码、Session、接口限流窗口这类"有 TTL 的临时数据",Redis 的过期机制正好对口。

一句话:把"高频、热点、短命"的数据从磁盘搬到内存,用空间换速度。

二、核心数据结构(先记这五种)

Redis 之所以强大,是因为它有五种常用的数据结构,各自解决一类问题:

类型底层实现典型场景
String(字符串)二进制安全,可存任意数据缓存对象(JSON)、计数器(INCR)、分布式锁(SET NX EX
Hash(哈希)一个 key 下挂多个 field-value存对象属性(用户信息、商品信息),比 String 省内存、可单独改字段
List(列表)双向链表消息队列(LPUSH/BRPOP)、最新列表(如时间线)
Set(集合)无序去重 + 集合运算去重(点赞用户)、交集/并集(共同好友)
ZSet(有序集合)带分数(score)排序排行榜(按分数排)、延时队列(按时间戳排)

几个记忆要点:

  • String 是万金油:缓存、计数、锁都靠它,用得最多也最基础。
  • Hash 适合"对象":改一个字段不用把整个 JSON 读出来重写,HINCRBY 还能对某个属性原子累加。
  • ZSet 是"带排序的 Set":每个成员带一个 score,ZRANGEBYSCORE 按分数区间取,排行榜和延时队列都靠这个。

顺带一提:Redis 的 List 也能当消息队列用,但它没有交换机、路由键、死信这些机制,属于"轻量队列"。需要路由、可靠性保障时用真正的 MQ(我整理的《RabbitMQ 快速入门》里有完整对比)。

三、数据持久化:RDB 和 AOF

Redis 是内存数据库,如果不做持久化,进程一挂数据全没。它提供两种持久化方式,可以只开一种也可以同时开:

方式原理优点缺点
RDB(快照)定期把内存数据全量生成二进制快照文件文件小、恢复快、对性能影响小两次快照之间的数据会丢
AOF(追加日志)把每次写命令追加到日志文件数据几乎不丢(可配每秒刷盘)文件大、恢复慢

生产环境的常见姿势:两种都开,AOF 保证数据不丢,RDB 用来快速恢复和备份。Redis 重启时优先用 AOF 恢复(因为更完整)。

四、缓存三大问题 + 一致性(重点)

用 Redis 当缓存,绕不开这四个经典问题,面试常问,生产常踩:

1. 缓存穿透:请求的数据缓存和数据库都没有(比如恶意查一个不存在的 ID),每次请求都直接打到数据库,Redis 形同虚设。

  • 解法:① 缓存空值(不存在也缓存一个短 TTL 的空结果);② 布隆过滤器(拦截不存在的 key)。

2. 缓存击穿:某个热点 key 突然过期,大量并发同时打到数据库。

  • 解法:① 热点数据不设过期时间(后台异步更新);② 互斥锁(只有一个请求去查库,其余等待)。

3. 缓存雪崩大量 key 在同一时间过期(或 Redis 挂了),请求全打到数据库,数据库被打垮。

  • 解法:① 过期时间加随机值,避免同时过期;② 热点数据多级缓存(本地缓存 + Redis);③ Redis 高可用(主从 + 哨兵/集群)。

4. 缓存与数据库一致性:先更新数据库还是先删缓存?

  • 主流方案是 Cache Aside(旁路缓存):读时先查缓存,没命中查库并回填;写时先更新数据库,再删除缓存(下次读时重新回填)。
  • 为什么不先删缓存再更新库?因为删缓存后、更新库前,可能有请求把旧数据写回缓存,导致缓存长期是脏的。先更新库再删缓存,配合延迟双删(删一次,等几百毫秒再删一次)能最大限度降低脏数据窗口。

五、Docker 安装 + 验证

本地开发用 Docker 一条命令起:

bash
docker run -d --name redis \
  -p 6379:6379 \
  redis:7
  • 6379:Redis 默认端口
  • 连进去验证:
bash
docker exec -it redis redis-cli
# 127.0.0.1:6379> set hello world
# OK
# 127.0.0.1:6379> get hello
# "world"

六、Spring Boot 集成实战

1. 依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 配置

yaml
spring:
  data:
    redis:
      host: localhost
      port: 6379

3. 基本读写(StringRedisTemplate)

Spring Boot 自动注入 RedisTemplateStringRedisTemplate,后者是 key/value 都是 String 的简化版,日常够用:

java
@Service
public class CacheService {

    @Autowired
    private StringRedisTemplate redis;

    /** 写缓存,带过期时间(5 分钟) */
    public void set(String key, String value) {
        redis.opsForValue().set(key, value, Duration.ofMinutes(5));
    }

    /** 读缓存,没命中返回 null */
    public String get(String key) {
        return redis.opsForValue().get(key);
    }
}

4. 缓存注解(@Cacheable)

最简单的玩法是直接加注解,Spring 帮你封装了"先查缓存、未命中查方法、回填缓存":

java
@Service
public class UserService {

    @Cacheable(cacheNames = "user", key = "#id")
    public User getUser(Long id) {
        // 模拟查数据库
        return userMapper.selectById(id);
    }

    @CacheEvict(cacheNames = "user", key = "#id")
    public void updateUser(Long id) {
        // 更新数据库后,删除缓存(Cache Aside 的"删缓存"一步)
        userMapper.updateById(...);
    }
}

注意:缓存注解默认用 JDK 序列化(key 有乱码前缀、value 不可读),生产要配置 RedisCacheManager 改用 JSON 序列化 + 统一 TTL。具体配置和注解的坑,见我整理的《Spring Cache 序列化与 TTL 配置》。

5. 分布式锁(最简实现)

跨多台服务器抢同一份资源(扣库存、抢优惠券)时,本地锁管不到别的机器,需要一把"全局互斥"的分布式锁。Redis 用 SET NX EX 一条命令原子实现:

java
// 尝试加锁:key 不存在才设置成功,30 秒自动过期防止死锁
Boolean locked = redis.opsForValue()
    .setIfAbsent("lock:stock:1001", "holder-" + requestId, Duration.ofSeconds(30));

if (Boolean.TRUE.equals(locked)) {
    try {
        // 业务逻辑:扣库存、发优惠券……
    } finally {
        // 释放锁:必须比对持有者 + Lua 原子删除,防止误删别人的锁
    }
}

这段只是最简版。它背后是一整条演进线——SETNX(死锁)→ 加 EXPIRE(非原子)→ SET NX EX(原子)→ Lua 原子释放 → Redisson(可重入 + 看门狗 + 高可用),涉及死锁、误删、锁续期、主从丢锁等问题,完整讲解见我整理的《Redis 分布式锁详解》。

七、入门就要知道的几个"坑"

  1. 别用 KEYS 命令KEYS * 会全量扫描阻塞 Redis(单线程,一阻塞全站卡住)。需要匹配时用 SCAN 游标分批。
  2. 小心大 Key(Big Key):一个 String 几 MB、一个 Hash 几十万字段,读写、删除、迁移都会阻塞。拆分成小 key,或定期扫描治理。
  3. Redis 单线程 ≠ 随便快:单线程保证原子性和简单,但 O(N) 命令(KEYS、大范围 LRANGE、大 Set 的 SMEMBERS)会堵住所有其他命令。
  4. 分布式锁别自己造轮子:锁过期、误删别人的锁、主从切换丢锁,每个坑都有成熟方案,直接用 Redisson。
  5. 缓存一致性没有银弹:Cache Aside + 延迟双删能解决大部分问题,但极端一致性要求下,该考虑"先更新库,通过 MQ 异步删缓存"甚至直接不缓存。

小结

  • Redis = 内存数据结构存储,五种核心结构:String(缓存/计数/锁)、Hash(对象)、List(轻量队列)、Set(去重/集合运算)、ZSet(排行榜/延时队列)
  • 持久化两种:RDB 快照(恢复快、可能丢数据)+ AOF 日志(几乎不丢、文件大),生产常同时开
  • 缓存三问:穿透(空值/布隆)、击穿(不设过期/互斥锁)、雪崩(随机过期/多级缓存)
  • 一致性主流方案:Cache Aside,先更新库再删缓存(可加延迟双删)
  • Spring Boot 集成三步:依赖 + 配置 + StringRedisTemplate(或缓存注解/分布式锁)
  • 生产避坑:不用 KEYS、治大 Key、锁用 Redisson

下一步可以看我的《秒杀系统架构分析》里 Redis 怎么做库存预热(setIfAbsent)和 Lua 原子扣减,那是 Redis 在真实高并发场景下的进阶用法。