46-Redis基础详解

2 阅读12分钟

Redis基础详解

一、知识概述

Redis 是一个开源的高性能键值对数据库,支持多种数据结构,具有持久化、过期策略、发布订阅等丰富特性。作为最流行的 NoSQL 数据库之一,Redis 在缓存、会话存储、排行榜、消息队列等场景中得到广泛应用。

本文将详细介绍 Redis 的核心概念,包括数据结构、持久化机制、过期策略等基础知识。

二、数据结构

2.1 五种基本数据类型

Redis 支持 5 种基本数据类型,每种类型都有特定的使用场景和操作命令。

String(字符串)

字符串是 Redis 最基本的数据类型,可以存储字符串、整数或浮点数。

// String 操作示例
import redis.clients.jedis.Jedis;

public class StringDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 基本操作
        jedis.set("name", "Redis");
        String value = jedis.get("name");
        System.out.println("name: " + value);
        
        // 数值操作
        jedis.set("counter", "100");
        jedis.incr("counter");  // 自增
        jedis.incrBy("counter", 10);  // 增加指定值
        jedis.decr("counter");  // 自减
        
        // 批量操作
        jedis.mset("key1", "value1", "key2", "value2");
        List<String> values = jedis.mget("key1", "key2");
        
        // 过期时间
        jedis.setex("temp", 60, "will expire in 60s");
        
        // 不存在时设置(分布式锁基础)
        jedis.setnx("lock", "locked");
        
        jedis.close();
    }
}

使用场景

  • 缓存对象(JSON 序列化)
  • 计数器
  • 分布式锁
  • Session 共享
List(列表)

列表是一个双向链表,支持在头部或尾部插入/删除元素。

// List 操作示例
public class ListDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 基本操作
        jedis.lpush("list", "a", "b", "c");  // 左侧插入
        jedis.rpush("list", "d", "e");  // 右侧插入
        List<String> list = jedis.lrange("list", 0, -1);  // 获取全部
        
        // 弹出元素
        String left = jedis.lpop("list");
        String right = jedis.rpop("list");
        
        // 阻塞弹出(消息队列)
        List<String> result = jedis.brpop(0, "queue");  // 0 表示永久阻塞
        
        // 获取列表长度
        long length = jedis.llen("list");
        
        jedis.close();
    }
}

使用场景

  • 消息队列
  • 最新列表
  • 时间线
Hash(哈希)

哈希是一个键值对集合,适合存储对象。

// Hash 操作示例
public class HashDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 存储对象
        Map<String, String> user = new HashMap<>();
        user.put("name", "张三");
        user.put("age", "25");
        user.put("city", "北京");
        jedis.hmset("user:1", user);
        
        // 获取单个字段
        String name = jedis.hget("user:1", "name");
        
        // 获取所有字段
        Map<String, String> userData = jedis.hgetAll("user:1");
        
        // 增加数值
        jedis.hincrBy("user:1", "age", 1);
        
        // 判断字段是否存在
        boolean exists = jedis.hexists("user:1", "name");
        
        // 删除字段
        jedis.hdel("user:1", "city");
        
        jedis.close();
    }
}

使用场景

  • 存储对象
  • 购物车
  • 计数器组
Set(集合)

集合是无序的唯一元素集合,支持集合运算。

// Set 操作示例
public class SetDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 添加元素
        jedis.sadd("tags", "java", "redis", "mysql");
        
        // 获取所有元素
        Set<String> tags = jedis.smembers("tags");
        
        // 判断元素是否存在
        boolean isMember = jedis.sismember("tags", "java");
        
        // 集合运算
        jedis.sadd("set1", "a", "b", "c");
        jedis.sadd("set2", "b", "c", "d");
        
        Set<String> intersection = jedis.sinter("set1", "set2");  // 交集
        Set<String> union = jedis.sunion("set1", "set2");  // 并集
        Set<String> diff = jedis.sdiff("set1", "set2");  // 差集
        
        jedis.close();
    }
}

使用场景

  • 标签系统
  • 共同好友
  • 唯一性判断
Sorted Set(有序集合)

有序集合是带分数的集合,元素按分数排序。

// Sorted Set 操作示例
public class SortedSetDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 添加元素(分数, 成员)
        jedis.zadd("rank", 100, "player1");
        jedis.zadd("rank", 200, "player2");
        jedis.zadd("rank", 150, "player3");
        
        // 获取排名(从小到大)
        Set<String> rank = jedis.zrange("rank", 0, -1);
        
        // 获取排名(从大到小)
        Set<String> rankDesc = jedis.zrevrange("rank", 0, 9);
        
        // 获取分数
        double score = jedis.zscore("rank", "player1");
        
        // 获取排名
        long rankIndex = jedis.zrevrank("rank", "player1");
        
        // 增加分数
        jedis.zincrby("rank", 50, "player1");
        
        // 范围查询(按分数)
        Set<String> range = jedis.zrangeByScore("rank", 100, 200);
        
        jedis.close();
    }
}

使用场景

  • 排行榜
  • 延时队列(时间戳作为分数)
  • 范围查询

2.2 高级数据类型

Bitmap(位图)
// Bitmap 操作示例
public class BitmapDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 设置位
        jedis.setbit("sign:2024:01", 0, true);  // 第1天签到
        jedis.setbit("sign:2024:01", 1, true);  // 第2天签到
        jedis.setbit("sign:2024:01", 2, false);  // 第3天未签到
        
        // 获取位
        boolean signed = jedis.getbit("sign:2024:01", 0);
        
        // 统计位为1的数量
        long count = jedis.bitcount("sign:2024:01");
        
        jedis.close();
    }
}

使用场景

  • 签到统计
  • 在线状态
  • 布隆过滤器
HyperLogLog
// HyperLogLog 操作示例
public class HyperLogLogDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 添加元素
        jedis.pfadd("uv:2024:01:01", "user1", "user2", "user3");
        jedis.pfadd("uv:2024:01:01", "user2", "user4");
        
        // 统计基数(约等于去重数量)
        long uv = jedis.pfcount("uv:2024:01:01");
        
        // 合并多个 HyperLogLog
        jedis.pfmerge("uv:2024:01", "uv:2024:01:01", "uv:2024:01:02");
        
        jedis.close();
    }
}

使用场景

  • UV 统计
  • 基数统计(误差约 0.81%)
Geo(地理位置)
// Geo 操作示例
public class GeoDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 添加位置(经度, 纬度, 名称)
        jedis.geoadd("locations", 116.404, 39.915, "北京");
        jedis.geoadd("locations", 121.474, 31.230, "上海");
        
        // 获取位置
        List<GeoCoordinate> coords = jedis.geopos("locations", "北京");
        
        // 计算距离
        double distance = jedis.geodist("locations", "北京", "上海", 
                                        GeoUnit.KM);
        
        // 查找附近位置
        List<GeoRadiusResponse> nearby = jedis.georadius("locations", 
                                                         116.404, 39.915, 
                                                         500, GeoUnit.KM);
        
        jedis.close();
    }
}

使用场景

  • 附近的人
  • 打车距离计算
  • 地理围栏

三、持久化机制

Redis 提供两种持久化方式:RDB 和 AOF,可以单独使用或组合使用。

3.1 RDB(快照)

RDB 是将某一时刻的内存数据保存到磁盘的二进制文件。

配置方式
# redis.conf 配置

# 保存触发条件
save 900 1      # 900秒内至少1个key变化
save 300 10     # 300秒内至少10个key变化
save 60 10000   # 60秒内至少10000个key变化

# RDB 文件名
dbfilename dump.rdb

# 存储目录
dir ./

# 压缩
rdbcompression yes

# 校验
rdbchecksum yes
触发方式
// 手动触发 RDB
public class RDBDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 同步保存(阻塞)
        jedis.save();
        
        // 异步保存(推荐)
        jedis.bgsave();
        
        // 获取上次保存时间
        long lastSave = jedis.lastsave();
        
        jedis.close();
    }
}

优点

  • 文件紧凑,适合备份
  • 恢复速度快
  • 对性能影响小(fork 子进程)

缺点

  • 可能丢失数据(两次保存之间的数据)
  • 数据量大时 fork 可能阻塞

3.2 AOF(Append Only File)

AOF 记录所有写操作命令,以文本格式追加到文件。

配置方式
# redis.conf 配置

# 开启 AOF
appendonly yes

# AOF 文件名
appendfilename "appendonly.aof"

# 同步策略
appendfsync always     # 每次写入都同步(最安全,最慢)
appendfsync everysec   # 每秒同步一次(推荐)
appendfsync no         # 由操作系统决定(最快,最不安全)

# AOF 重写触发条件
auto-aof-rewrite-percentage 100  # 文件大小比上次重写后增长100%
auto-aof-rewrite-min-size 64mb   # 文件至少64MB

# 重写时是否同步
no-appendfsync-on-rewrite no
AOF 重写
// 手动触发 AOF 重写
public class AOFDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 手动重写
        jedis.bgrewriteaof();
        
        jedis.close();
    }
}

重写原理

  1. Redis fork 子进程
  2. 子进程基于当前内存数据生成新的 AOF 文件
  3. 父进程继续处理命令,写入 AOF 缓冲区和重写缓冲区
  4. 子进程完成后,父进程将重写缓冲区追加到新文件
  5. 原子性地用新文件替换旧文件

优点

  • 数据安全性高
  • 可读性好,便于分析
  • 支持重写压缩

缺点

  • 文件体积大
  • 恢复速度较慢
  • 写入性能略低于 RDB

3.3 混合持久化

Redis 4.0 引入混合持久化,结合 RDB 和 AOF 的优点。

# redis.conf 配置

# 开启混合持久化
aof-use-rdb-preamble yes

工作原理

  1. AOF 重写时,先写入 RDB 格式的快照
  2. 再追加增量的 AOF 命令
  3. 恢复时先加载 RDB 部分,再执行 AOF 命令

优点

  • 恢复速度快
  • 数据丢失少

四、过期策略

4.1 过期时间设置

// 过期时间设置示例
public class ExpireDemo {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        // 设置过期时间(秒)
        jedis.setex("key1", 60, "value");  // 创建时设置
        jedis.expire("key2", 60);  // 已存在的key
        
        // 设置过期时间(毫秒)
        jedis.pexpire("key3", 60000);
        
        // 设置过期时间戳(Unix 时间戳)
        jedis.expireAt("key4", System.currentTimeMillis() / 1000 + 60);
        
        // 查看剩余生存时间
        long ttl = jedis.ttl("key1");  // 秒
        long pttl = jedis.pttl("key1");  // 毫秒
        
        // 取消过期时间
        jedis.persist("key1");
        
        jedis.close();
    }
}

4.2 过期策略详解

定时删除

为每个设置了过期时间的 key 创建定时器,到期立即删除。

优点:内存及时释放 缺点:CPU 消耗大,不推荐

惰性删除

访问 key 时检查是否过期,过期则删除。

// 惰性删除伪代码
public String get(String key) {
    String value = db.get(key);
    if (value != null && isExpired(key)) {
        db.delete(key);
        return null;
    }
    return value;
}

优点:CPU 消耗少 缺点:可能占用内存较久

定期删除

定期随机检查一部分设置了过期时间的 key,删除其中已过期的。

配置参数

# redis.conf 配置

# 每秒执行多少次过期检查
hz 10  # 默认每秒10次

# 每次检查的过期键数量
# 由 hz 和 active-expire-cycle 配置决定

工作流程

  1. 从过期字典中随机选取部分 key
  2. 检查是否过期,过期则删除
  3. 如果过期 key 比例超过 25%,继续检查
  4. 每次检查时间限制(避免阻塞)
Redis 的策略

Redis 采用惰性删除 + 定期删除的组合策略:

  1. 惰性删除:保证只删除过期的 key
  2. 定期删除:防止过期 key 占用太多内存

4.3 内存淘汰策略

当内存达到上限时,Redis 提供多种淘汰策略:

# redis.conf 配置

# 最大内存
maxmemory 1gb

# 淘汰策略
maxmemory-policy allkeys-lru

淘汰策略说明

策略说明
noeviction不淘汰,内存满时返回错误(默认)
allkeys-lru所有 key 中,淘汰最近最少使用的
volatile-lru设置了过期时间的 key 中,淘汰最近最少使用的
allkeys-random所有 key 中,随机淘汰
volatile-random设置了过期时间的 key 中,随机淘汰
volatile-ttl设置了过期时间的 key 中,淘汰即将过期的
allkeys-lfu所有 key 中,淘汰最不经常使用的(Redis 4.0+)
volatile-lfu设置了过期时间的 key 中,淘汰最不经常使用的(Redis 4.0+)

选择建议

  • 缓存场景:allkeys-lru
  • 有冷热数据:allkeys-lfu
  • 有过期时间:volatile-lru
  • 不能丢数据:noeviction

五、实战应用场景

5.1 缓存

// 缓存模式示例
public class CacheDemo {
    private Jedis jedis;
    
    public CacheDemo() {
        jedis = new Jedis("localhost", 6379);
    }
    
    // 缓存穿透防护:布隆过滤器 + 空值缓存
    public String getDataWithBloomFilter(String key) {
        // 1. 先检查布隆过滤器
        if (!bloomFilterContains(key)) {
            return null;
        }
        
        // 2. 检查缓存
        String value = jedis.get(key);
        if (value != null) {
            return value;
        }
        
        // 3. 查询数据库
        value = queryFromDB(key);
        
        // 4. 写入缓存
        if (value != null) {
            jedis.setex(key, 3600, value);
        } else {
            // 空值缓存,防止缓存穿透
            jedis.setex(key, 60, "");
        }
        
        return value;
    }
    
    // 缓存雪崩防护:随机过期时间
    public void cacheWithRandomExpire(String key, String value, int baseExpire) {
        int randomExpire = baseExpire + new Random().nextInt(300);
        jedis.setex(key, randomExpire, value);
    }
    
    // 缓存击穿防护:互斥锁
    public String getDataWithLock(String key) {
        String value = jedis.get(key);
        if (value != null) {
            return value;
        }
        
        String lockKey = "lock:" + key;
        try {
            // 尝试获取锁
            if (jedis.setnx(lockKey, "1") == 1) {
                jedis.expire(lockKey, 10);
                
                // 查询数据库
                value = queryFromDB(key);
                
                // 写入缓存
                if (value != null) {
                    jedis.setex(key, 3600, value);
                }
            } else {
                // 等待后重试
                Thread.sleep(50);
                return getDataWithLock(key);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            jedis.del(lockKey);
        }
        
        return value;
    }
    
    private boolean bloomFilterContains(String key) {
        // 实现布隆过滤器检查
        return true;
    }
    
    private String queryFromDB(String key) {
        // 模拟数据库查询
        return "data:" + key;
    }
}

5.2 分布式锁

// 分布式锁实现
public class DistributedLock {
    private Jedis jedis;
    private String lockKey;
    private String lockValue;
    private int expireTime = 30;  // 秒
    
    public DistributedLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString();
    }
    
    // 尝试获取锁
    public boolean tryLock() {
        String result = jedis.set(lockKey, lockValue, 
                                   "NX", "EX", expireTime);
        return "OK".equals(result);
    }
    
    // 尝试获取锁(带超时)
    public boolean tryLock(long timeout, TimeUnit unit) {
        long endTime = System.currentTimeMillis() + unit.toMillis(timeout);
        while (System.currentTimeMillis() < endTime) {
            if (tryLock()) {
                return true;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        return false;
    }
    
    // 释放锁(Lua 脚本保证原子性)
    public void unlock() {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        jedis.eval(script, 1, lockKey, lockValue);
    }
}

5.3 排行榜

// 排行榜实现
public class LeaderboardDemo {
    private Jedis jedis;
    
    public LeaderboardDemo() {
        jedis = new Jedis("localhost", 6379);
    }
    
    // 更新分数
    public void updateScore(String leaderboard, String player, double score) {
        jedis.zadd(leaderboard, score, player);
    }
    
    // 获取排行榜(前N名)
    public List<RankInfo> getTopN(String leaderboard, int n) {
        Set<Tuple> tuples = jedis.zrevrangeWithScores(leaderboard, 0, n - 1);
        List<RankInfo> result = new ArrayList<>();
        int rank = 1;
        for (Tuple tuple : tuples) {
            result.add(new RankInfo(rank++, tuple.getElement(), tuple.getScore()));
        }
        return result;
    }
    
    // 获取玩家排名
    public RankInfo getPlayerRank(String leaderboard, String player) {
        Long rank = jedis.zrevrank(leaderboard, player);
        Double score = jedis.zscore(leaderboard, player);
        if (rank != null && score != null) {
            return new RankInfo(rank.intValue() + 1, player, score);
        }
        return null;
    }
    
    // 获取玩家周围的排名
    public List<RankInfo> getAround(String leaderboard, String player, int range) {
        Long rank = jedis.zrevrank(leaderboard, player);
        if (rank == null) {
            return Collections.emptyList();
        }
        
        long start = Math.max(0, rank - range);
        long end = rank + range;
        
        Set<Tuple> tuples = jedis.zrevrangeWithScores(leaderboard, start, end);
        List<RankInfo> result = new ArrayList<>();
        int currentRank = (int) start + 1;
        for (Tuple tuple : tuples) {
            result.add(new RankInfo(currentRank++, tuple.getElement(), tuple.getScore()));
        }
        return result;
    }
    
    static class RankInfo {
        int rank;
        String player;
        double score;
        
        RankInfo(int rank, String player, double score) {
            this.rank = rank;
            this.player = player;
            this.score = score;
        }
        
        @Override
        public String toString() {
            return String.format("#%d %s: %.2f", rank, player, score);
        }
    }
}

六、总结与最佳实践

6.1 数据结构选择

场景推荐数据结构说明
简单缓存String最常用,适合存储对象
对象属性Hash字段级别的读写
消息队列ListLPUSH + BRPOP
最新列表ListLPUSH + LRANGE
标签/集合Set交集、并集、差集
排行榜Sorted Set分数排序
签到统计Bitmap节省空间
UV 统计HyperLogLog误差小,占用小
附近的人Geo地理位置计算

6.2 持久化策略选择

场景推荐策略
允许分钟级数据丢失RDB
不允许数据丢失AOF
平衡性能和数据安全RDB + AOF
大数据量恢复RDB + AOF 混合

6.3 性能优化建议

  1. 内存优化

    • 合理设置过期时间
    • 使用合适的数据结构
    • 配置内存淘汰策略
  2. 网络优化

    • 使用 Pipeline 批量操作
    • 使用 Lua 脚本减少往返
    • 合理配置连接池
  3. 持久化优化

    • RDB 在低峰期执行
    • AOF 使用 everysec 策略
    • 合理配置重写触发条件

6.4 注意事项

  1. 避免大 Key

    • 单个 key 的 value 不超过 10KB
    • 集合类型元素不超过 5000 个
    • 使用 hash 拆分大对象
  2. 避免热点 Key

    • 使用本地缓存
    • 拆分热点数据
  3. 合理使用过期时间

    • 避免同时大量过期
    • 添加随机偏移
  4. 监控告警

    • 内存使用率
    • 慢查询日志
    • 过期键数量

七、思考与练习

思考题

  1. 基础题:Redis 的五种基本数据类型分别适合什么场景?请各举一个实际应用例子。

  2. 进阶题:RDB 和 AOF 持久化各有什么优缺点?在生产环境中应该如何选择持久化策略?

  3. 实战题:如何设计一个防止缓存穿透、缓存击穿、缓存雪崩的缓存方案?请说明每种问题的防护措施。

编程练习

练习:使用 Redis 实现一个简单的分布式限流器,要求:

  • 支持每分钟最多 N 次请求
  • 使用滑动窗口算法
  • 返回是否允许访问及剩余次数

提示:可以使用 Sorted Set 存储请求时间戳,通过 ZREMRANGEBYSCORE 清理过期数据。

章节关联

  • 前置章节:无
  • 后续章节:《Redis集群详解》- 学习 Redis 集群部署和高可用方案
  • 扩展阅读:《Redis 设计与实现》、《Redis 开发与运维》

📝 下一章预告

下一章将深入探讨 Redis 集群技术,包括主从复制、哨兵模式、Cluster 集群的原理与实践,帮助你构建高可用的 Redis 服务。


本章完