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();
}
}
重写原理:
- Redis fork 子进程
- 子进程基于当前内存数据生成新的 AOF 文件
- 父进程继续处理命令,写入 AOF 缓冲区和重写缓冲区
- 子进程完成后,父进程将重写缓冲区追加到新文件
- 原子性地用新文件替换旧文件
优点:
- 数据安全性高
- 可读性好,便于分析
- 支持重写压缩
缺点:
- 文件体积大
- 恢复速度较慢
- 写入性能略低于 RDB
3.3 混合持久化
Redis 4.0 引入混合持久化,结合 RDB 和 AOF 的优点。
# redis.conf 配置
# 开启混合持久化
aof-use-rdb-preamble yes
工作原理:
- AOF 重写时,先写入 RDB 格式的快照
- 再追加增量的 AOF 命令
- 恢复时先加载 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 配置决定
工作流程:
- 从过期字典中随机选取部分 key
- 检查是否过期,过期则删除
- 如果过期 key 比例超过 25%,继续检查
- 每次检查时间限制(避免阻塞)
Redis 的策略
Redis 采用惰性删除 + 定期删除的组合策略:
- 惰性删除:保证只删除过期的 key
- 定期删除:防止过期 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 | 字段级别的读写 |
| 消息队列 | List | LPUSH + BRPOP |
| 最新列表 | List | LPUSH + LRANGE |
| 标签/集合 | Set | 交集、并集、差集 |
| 排行榜 | Sorted Set | 分数排序 |
| 签到统计 | Bitmap | 节省空间 |
| UV 统计 | HyperLogLog | 误差小,占用小 |
| 附近的人 | Geo | 地理位置计算 |
6.2 持久化策略选择
| 场景 | 推荐策略 |
|---|---|
| 允许分钟级数据丢失 | RDB |
| 不允许数据丢失 | AOF |
| 平衡性能和数据安全 | RDB + AOF |
| 大数据量恢复 | RDB + AOF 混合 |
6.3 性能优化建议
-
内存优化
- 合理设置过期时间
- 使用合适的数据结构
- 配置内存淘汰策略
-
网络优化
- 使用 Pipeline 批量操作
- 使用 Lua 脚本减少往返
- 合理配置连接池
-
持久化优化
- RDB 在低峰期执行
- AOF 使用 everysec 策略
- 合理配置重写触发条件
6.4 注意事项
-
避免大 Key
- 单个 key 的 value 不超过 10KB
- 集合类型元素不超过 5000 个
- 使用 hash 拆分大对象
-
避免热点 Key
- 使用本地缓存
- 拆分热点数据
-
合理使用过期时间
- 避免同时大量过期
- 添加随机偏移
-
监控告警
- 内存使用率
- 慢查询日志
- 过期键数量
七、思考与练习
思考题
-
基础题:Redis 的五种基本数据类型分别适合什么场景?请各举一个实际应用例子。
-
进阶题:RDB 和 AOF 持久化各有什么优缺点?在生产环境中应该如何选择持久化策略?
-
实战题:如何设计一个防止缓存穿透、缓存击穿、缓存雪崩的缓存方案?请说明每种问题的防护措施。
编程练习
练习:使用 Redis 实现一个简单的分布式限流器,要求:
- 支持每分钟最多 N 次请求
- 使用滑动窗口算法
- 返回是否允许访问及剩余次数
提示:可以使用 Sorted Set 存储请求时间戳,通过 ZREMRANGEBYSCORE 清理过期数据。
章节关联
- 前置章节:无
- 后续章节:《Redis集群详解》- 学习 Redis 集群部署和高可用方案
- 扩展阅读:《Redis 设计与实现》、《Redis 开发与运维》
📝 下一章预告
下一章将深入探讨 Redis 集群技术,包括主从复制、哨兵模式、Cluster 集群的原理与实践,帮助你构建高可用的 Redis 服务。
本章完