Redis实践

Posted by     "zengchengjie" on Monday, April 4, 2022

Redis 核心实践指南:从数据结构到生产实战

前言

Redis 凭借其卓越的性能和丰富的数据结构,已经成为现代后端架构中不可或缺的一环。它既是性能加速器,也是解决复杂业务问题的利器。

本文将从实际开发角度出发,系统性地介绍 Redis 的核心数据结构、高级特性以及生产环境中常见问题的解决方案。

一、Redis 是什么?

根据 Redis 官方文档的定义:

Redis 是一个开源(BSD许可)的内存数据结构存储,可用作数据库、缓存、消息代理和流引擎。它提供字符串、哈希、列表、集合、带范围查询的有序集合、位图、HyperLogLog、地理空间索引和流等数据结构。

简单来说,Redis 是一个跑在内存里的"数据工具箱",它不仅能存数据,还能对数据进行各种高效操作。

核心能力一览

能力 说明
内存存储 数据主要在内存中,读写速度极快
数据结构丰富 不仅仅是 Key-Value,还有 List、Hash、Set 等
原子操作 所有操作都是原子的,支持复杂的数据操作
持久化 支持 RDB 快照和 AOF 日志两种持久化方式
高可用 支持主从复制、Sentinel 哨兵、Cluster 集群

二、快速部署

Docker 单机部署

docker run -itd -p 6379:6379 --restart always --name redis \
  redis --requirepass "your_password"

Docker Compose 部署(推荐生产使用)

version: '3'
services:
  redis:
    image: redis:7.0
    container_name: redis
    volumes:
      - ./data:/data
      - ./redis.conf:/usr/local/etc/redis/redis.conf
      - ./logs:/logs
    command: redis-server /usr/local/etc/redis/redis.conf --requirepass your_password
    ports:
      - "6379:6379"
    restart: always

常用运维命令

# 进入 Redis CLI
docker exec -it redis redis-cli -a your_password

# 查看所有 key(生产环境慎用)
KEYS *

# 查看 key 类型
TYPE key_name

# 设置过期时间(秒)
EXPIRE key 60

# 查看剩余过期时间(-1 表示永不过期,-2 表示不存在)
TTL key

# 删除 key
DEL key

# 清空当前库
FLUSHDB

# 清空所有库
FLUSHALL

# 批量删除指定前缀的 key
redis-cli -a password KEYS "prefix_*" | xargs redis-cli -a password DEL

三、核心数据结构

3.1 String(字符串)

底层实现

  • 整数值且可用 long 表示 → int 编码
  • 字符串长度 ≤ 39 字节 → embstr 编码
  • 字符串长度 > 39 字节 → raw 编码(SDS)

基本操作

# 基础读写
SET hello world
GET hello

# 批量操作
MSET a 10 b 20 c 30
MGET a b c

# 带过期时间的写入(重要!)
SET key value EX 10        # 10秒后过期
SET key value PX 10000     # 10000毫秒后过期

# 条件写入(实现分布式锁的关键)
SET key value NX           # 仅当 key 不存在时设置
SET key value XX           # 仅当 key 存在时设置

# 组合使用:实现锁的最佳实践
SET lock_key unique_value NX EX 10

# 数值操作
INCR counter
DECR counter
INCRBY counter 100

# 判断存在
EXISTS key

应用场景

  • 缓存热点数据(用户信息、配置等)
  • 分布式锁(SET NX EX 组合)
  • 计数器(访问量、点赞数)
  • 分布式 ID 生成

3.2 List(列表)

特点:有序、可重复、双端操作高效

底层实现:元素长度 < 64 字节且数量 < 512 时使用 ziplist,否则使用 linkedlist

# 插入
LPUSH list a b c    # 左侧插入
RPUSH list x y z    # 右侧插入

# 查询
LRANGE list 0 -1    # 获取全部
LRANGE list 0 10    # 分页查询

# 弹出(取出并删除)
LPOP list           # 左侧弹出
RPOP list           # 右侧弹出

# 阻塞弹出(实现消息队列的关键)
BLPOP list 5        # 阻塞5秒,没有元素则超时返回
BRPOP list 0        # 永久阻塞,直到有元素

# 长度
LLEN list

应用场景

  • 消息队列:LPUSH + BRPOP 实现生产者-消费者模式
  • 分页数据:LRANGE 天然支持分页
  • 最新消息/时间线:如微博最新动态
  • 栈/队列:LPUSH/LPOP 实现栈,RPUSH/LPOP 实现队列

3.3 Hash(哈希)

特点:适合存储对象,支持对单个字段的操作

底层实现:字段长度 < 64 字节且数量 < 512 时使用 ziplist,否则使用 hashtable

# 设置字段
HSET user:1001 name "张三" age 25
HSET user:1001 email "zhangsan@example.com"

# 获取字段
HGET user:1001 name
HGETALL user:1001           # 获取全部(慎用,数据量大时性能差)

# 批量操作
HMSET user:1002 name "李四" age 30
HMGET user:1002 name age

# 数值操作
HINCRBY user:1001 age 1

# 判断字段存在
HEXISTS user:1001 name

# 删除字段
HDEL user:1001 email

应用场景

  • 对象存储:相比 String 存储 JSON,Hash 支持单独更新字段,减少网络传输
  • 购物车:key=用户ID,field=商品ID,value=商品数量
  • 计数器组:同一用户的多个计数指标

3.4 Set(集合)

特点:无序、不可重复、支持集合运算

底层实现:元素都是整数且数量 ≤ 512 时使用 intset,否则使用 hashtable

# 添加
SADD fruits apple banana orange

# 查询
SMEMBERS fruits           # 获取全部(慎用)
SISMEMBER fruits apple    # 判断存在

# 删除
SREM fruits apple

# 集合运算
SINTER set1 set2          # 交集
SUNION set1 set2          # 并集
SDIFF set1 set2           # 差集(set1 有但 set2 没有)

# 随机操作
SRANDMEMBER set 2         # 随机获取2个元素
SPOP set                  # 随机弹出并删除

# 数量
SCARD set

应用场景

  • 标签系统:用户标签、文章标签
  • 社交关系:共同好友、关注/粉丝集合
  • 抽奖/去重:随机抽取不重复用户
  • 黑白名单:快速判断存在性

3.5 Sorted Set(有序集合)

特点:唯一成员 + 可排序分数,Redis 中最强大的数据结构之一

底层实现:元素数量 < 128 且成员长度 < 64 字节时使用 ziplist,否则使用 skiplist

# 添加(分数在前,成员在后)
ZADD leaderboard 100 "player1" 95 "player2" 88 "player3"

# 查询
ZRANGE leaderboard 0 -1 WITHSCORES      # 升序
ZREVRANGE leaderboard 0 -1 WITHSCORES   # 降序

# 按分数范围查询
ZRANGEBYSCORE leaderboard 90 100        # 90-100分

# 按排名查询
ZRANK leaderboard "player1"             # 获取排名(0-based)
ZREVRANK leaderboard "player1"          # 获取反向排名

# 获取分数
ZSCORE leaderboard "player1"

# 增加分数
ZINCRBY leaderboard 10 "player1"

# 删除
ZREM leaderboard "player3"

# 获取数量
ZCARD leaderboard
ZCOUNT leaderboard 90 100               # 分数区间内数量

应用场景

  • 排行榜:游戏积分榜、热搜榜、销量榜
  • 带权重的队列:分数越高优先级越高
  • 延迟队列:用时间戳作为分数
  • 范围查询:地理位置距离、时间范围

3.6 HyperLogLog(基数统计)

特点:极省内存(每个 key 约 12KB),统计近似基数,误差率约 0.81%

# 添加元素
PFADD uv:2024-01-01 user1 user2 user3

# 统计基数
PFCOUNT uv:2024-01-01

# 合并多个 HyperLogLog
PFMERGE uv:2024-01 uv:2024-01-01 uv:2024-01-02

应用场景

  • UV 统计:网站独立访客数
  • 去重计数:搜索关键词去重统计
  • 大规模去重场景:牺牲一定精度换取内存

3.7 GEO(地理空间)

特点:存储地理位置信息并支持距离计算

# 添加地理位置(经度 纬度 名称)
GEOADD cities 116.405285 39.912835 "北京" 121.473701 31.230416 "上海"

# 获取位置
GEOPOS cities 北京 上海

# 计算距离(单位:m/km/mi/ft)
GEODIST cities 北京 上海 km

# 半径查询(以北京为中心,半径1000km内的城市)
GEORADIUS cities 116.405285 39.912835 1000 km WITHDIST

应用场景

  • 附近的人/店铺
  • 打车/外卖距离计算
  • 地图相关功能

3.8 Bitmap(位图)

特点:按位存储,极致节省空间,1个字节可存8个状态

# 设置某一位
SETBIT sign:202401 0 1    # 第0位设为1(第1天签到)
SETBIT sign:202401 1 1    # 第1位设为1(第2天签到)

# 获取某一位
GETBIT sign:202401 0

# 统计1的个数
BITCOUNT sign:202401

# 位运算
BITOP AND result key1 key2

应用场景

  • 签到系统:365天只需 46 字节
  • 在线状态:用户 ID 作为偏移量
  • 布隆过滤器:底层实现

四、高级特性

4.1 布隆过滤器(Bloom Filter)

概述:Bloom Filter 是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能在集合中。

核心原理

  1. 一个二进制向量(初始全为 0)
  2. 多个 Hash 函数
  3. 添加元素时,将多个 Hash 值对应的位设为 1
  4. 查询时,如果任意一个对应位为 0,则元素一定不存在;如果全为 1,则元素可能存在

特点

  • ✅ 极省内存
  • ✅ 查询速度快
  • ❌ 存在误判率(可能把不存在判断为存在)
  • ❌ 不支持删除

使用 RedisBloom 插件

# 添加元素
BF.ADD bloom_key element

# 判断存在
BF.EXISTS bloom_key element

# 批量操作
BF.MADD bloom_key e1 e2 e3
BF.MEXISTS bloom_key e1 e2 e3

Java 使用 Redisson

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setPassword("password");
RedissonClient redisson = Redisson.create(config);

RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phone_blacklist");
// 预计元素 5 亿,误差率 3%
bloomFilter.tryInit(500000000L, 0.03);

bloomFilter.add("13800138000");
System.out.println(bloomFilter.contains("13800138000")); // true

应用场景

  • 缓存穿透防护:将存在的数据 ID 放入布隆过滤器,不存在直接拦截
  • 黑名单校验:IP 黑名单、手机号黑名单
  • 爬虫去重:URL 去重

4.2 内存淘汰策略

当 Redis 内存达到上限时,需要按照配置的策略淘汰数据。

策略 说明
noeviction 不淘汰,写入操作报错(默认)
allkeys-lru 所有 key 中淘汰最近最少使用的
allkeys-random 所有 key 中随机淘汰
volatile-lru 设置了过期时间的 key 中淘汰 LRU
volatile-random 设置了过期时间的 key 中随机淘汰
volatile-ttl 设置了过期时间的 key 中淘汰 TTL 最小的
allkeys-lfu (4.0+)所有 key 中淘汰最不经常使用的
volatile-lfu (4.0+)设置了过期时间的 key 中淘汰 LFU

配置方式:maxmemory-policy allkeys-lru

五、生产常见问题与解决方案

5.1 缓存雪崩

现象:大量缓存同时失效,请求直接打到数据库,导致数据库压力骤增甚至宕机。

原因

  • 大量 key 设置了相同的过期时间
  • Redis 实例宕机

解决方案

方案 说明
过期时间打散 在基础时间上加随机偏移量
热点数据永不过期 通过后台任务异步更新
高可用部署 主从+哨兵/Cluster 集群
限流降级 使用 Sentinel/Hystrix 做熔断限流
// 过期时间打散示例
int baseExpire = 3600;
int randomOffset = new Random().nextInt(300); // 0-300秒随机
int finalExpire = baseExpire + randomOffset;
redisTemplate.expire(key, finalExpire, TimeUnit.SECONDS);

5.2 缓存穿透

现象:请求的数据在缓存和数据库中都不存在,每次请求都穿透到数据库。

解决方案

方案 说明
参数校验 在接口层拦截无效参数(如 id ≤ 0)
缓存空对象 将不存在的 key 也缓存,设置较短过期时间
布隆过滤器 提前将可能存在的数据放入 Bloom Filter
// 缓存空对象方案
User user = dao.getUserById(id);
if (user == null) {
    redisTemplate.opsForValue().set(cacheKey, "NULL", 60, TimeUnit.SECONDS);
    return null;
}

5.3 缓存击穿

现象:某个热点 key 失效的瞬间,大量并发请求同时打到数据库。

解决方案

方案 说明
热点数据永不过期 不设置过期时间,由后台任务主动更新
互斥锁 只允许一个线程去加载数据
// 互斥锁方案
public User getHotUser(Long id) {
    String cacheKey = "user:" + id;
    User user = redisTemplate.opsForValue().get(cacheKey);
    if (user != null) {
        return user;
    }
    
    String lockKey = "lock:user:" + id;
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    
    if (Boolean.TRUE.equals(locked)) {
        try {
            // 加载数据库
            user = dao.getUserById(id);
            redisTemplate.opsForValue().set(cacheKey, user, 3600, TimeUnit.SECONDS);
        } finally {
            redisTemplate.delete(lockKey);
        }
    } else {
        // 等待重试
        Thread.sleep(100);
        return getHotUser(id);
    }
    return user;
}

5.4 缓存与数据库双写一致性

问题:更新数据库后,如何保证缓存数据的一致性?

常见方案

方案 适用场景 说明
先更新数据库,再删除缓存 大部分场景(推荐) 最简洁,并发下有短暂不一致
先删除缓存,再更新数据库 能容忍短暂不一致 可能被旧数据回填
延迟双删 对一致性要求较高 更新后休眠再删一次
订阅 Binlog(Canal) 强一致性要求 通过监听数据库变更同步缓存
// 推荐方案:先更新 DB,再删缓存
@Transactional
public void updateUser(User user) {
    userDao.updateById(user);
    redisTemplate.delete("user:" + user.getId());
}

5.5 热 Key / 大 Key 问题

热 Key:某个 key 被超高频率访问。

大 Key:key 对应的 value 过大(String 超过 10KB,集合元素超过 5000)。

问题类型 危害 解决方案
热 Key 单节点 CPU 飙高 本地缓存、读写分离、多副本
大 Key 网络阻塞、慢查询 拆分、压缩、换数据结构

大 Key 排查命令

# 查看各 key 的内存占用
redis-cli --bigkeys

# 查看某个 key 的大小
MEMORY USAGE key_name
DEBUG OBJECT key_name

六、最佳实践总结

6.1 命名规范

[业务名]:[模块名]:[实体]:[ID]:[属性]

# 示例
order:pay:status:12345
user:profile:1001:basic

6.2 Key 设计原则

  • ✅ 语义清晰,层级分明
  • ✅ 控制长度,不要超过 1KB
  • ✅ 设置合理的过期时间
  • ❌ 避免使用 KEYS 命令
  • ❌ 避免存储大 Value
  • ❌ 避免使用 FLUSHALL/FLUSHDB

6.3 性能优化要点

  1. 使用 Pipeline:批量操作减少 RTT
  2. 使用连接池:避免频繁创建连接
  3. 合理设置过期时间:避免内存无限增长
  4. 选择合适的结构:Hash 代替 String 存储对象
  5. 读写分离:主写从读

七、总结

Redis 的精髓在于:用最合适的数据结构,解决最恰当的业务问题

场景 推荐结构
缓存对象 String(JSON)或 Hash
计数器 String(INCR)
排行榜 Sorted Set
消息队列 List(LPUSH + BRPOP)
标签/好友 Set
UV 统计 HyperLogLog
附近的人 GEO
签到 Bitmap
防缓存穿透 Bloom Filter

掌握 Redis 不仅仅是会用几个命令,更重要的是理解每个数据结构的底层特性和适用边界,这样才能在合适的场景做出最优的选择。