Redis数据类型完全指南:从String到Stream的全面解读——底层实现与使用场景
·
Redis数据类型完全指南:从String到Stream的全面解读——底层实现与使用场景
|
🌺The Begin🌺点点关注,收藏不迷路🌺
|
📌 前言
Redis之所以被称为“数据结构服务器”,是因为它不仅仅支持简单的Key-Value存储,还提供了丰富的数据类型。理解这些数据类型及其底层实现,是高效使用Redis的关键。本文将全面介绍Redis的9种数据类型,深入剖析它们的底层数据结构、适用场景以及最佳实践。
一、数据类型概览
数据类型对比表
| 类型 | 存储结构 | 最大元素 | 适用场景 | 底层实现 |
|---|---|---|---|---|
| String | 键值对 | 512MB | 缓存、计数器、分布式锁 | SDS |
| Hash | 字段-值 | 2^32-1 | 对象存储 | ZipList/Dict |
| List | 双向链表 | 2^32-1 | 消息队列、最新列表 | QuickList |
| Set | 无序集合 | 2^32-1 | 标签、去重、交集 | IntSet/Dict |
| ZSet | 有序集合 | 2^32-1 | 排行榜、延时队列 | ZipList/SkipList |
| Bitmap | 位数组 | 2^32位 | 签到、布隆过滤器 | SDS |
| HyperLogLog | 概率统计 | 2^64 | UV统计 | 特殊结构 |
| Geo | 地理位置 | 2^32-1 | 附近的人 | ZSet |
| Stream | 消息流 | 无限制 | 消息队列 | Listpack+Rax |
二、String(字符串)
2.1 底层实现:SDS(Simple Dynamic String)
// Redis SDS 结构
struct sdshdr {
int len; // 已使用字节数
int alloc; // 分配字节数
unsigned char flags; // SDS类型
char buf[]; // 字符数组
};
2.2 常用命令
# 基础操作
SET user:1 "John"
GET user:1
DEL user:1
# 带过期时间
SETEX session:abc 3600 "user_data"
SET session:abc "user_data" EX 3600 NX
# 批量操作
MSET a 1 b 2 c 3
MGET a b c
# 数值操作
INCR counter
INCRBY counter 10
DECR counter
DECRBY counter 5
# 字符串操作
APPEND key " suffix"
STRLEN key
GETRANGE key 0 10
SETRANGE key 5 "new"
2.3 应用场景
| 场景 | 示例 | 命令 |
|---|---|---|
| 对象缓存 | 用户信息JSON | SET user:1001 ‘{“name”:“John”}’ |
| 计数器 | 文章阅读量 | INCR article:123:views |
| 分布式锁 | 锁实现 | SET lock:key value NX EX 30 |
| Session存储 | 用户会话 | SETEX session:token 3600 data |
| 限流器 | 滑动窗口 | INCR + EXPIRE |
// 分布式锁最佳实践
public boolean tryLock(String key, String value, int expireSeconds) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, Duration.ofSeconds(expireSeconds));
return Boolean.TRUE.equals(result);
}
三、Hash(哈希表)
3.1 底层实现
# Hash vs String 存储对象对比
# String方式:需要多次序列化/反序列化
SET user:1001 '{"id":1001,"name":"John","age":25}'
# Hash方式:可直接操作字段
HSET user:1001 id 1001 name John age 25
HGET user:1001 name # 直接获取name字段
3.2 常用命令
# 基本操作
HSET user:1001 name "John" age 25
HGET user:1001 name
HGETALL user:1001
# 批量操作
HMSET user:1001 email "john@test.com" phone "123456"
HMGET user:1001 name email
# 数值操作
HINCRBY user:1001 age 1
HINCRBYFLOAT product:1 price -5.5
# 判断与删除
HEXISTS user:1001 name
HDEL user:1001 phone
# 获取所有字段/值
HKEYS user:1001
HVALS user:1001
HLEN user:1001
# 迭代(支持大数据量)
HSCAN user:1001 0 MATCH name*
3.3 应用场景
// 购物车实现
@Service
public class CartService {
private static final String CART_KEY_PREFIX = "cart:user:";
// 添加商品到购物车
public void addToCart(Long userId, Long productId, Integer quantity) {
String key = CART_KEY_PREFIX + userId;
redisTemplate.opsForHash().increment(key, productId.toString(), quantity);
}
// 获取购物车所有商品
public Map<Object, Object> getCart(Long userId) {
String key = CART_KEY_PREFIX + userId;
return redisTemplate.opsForHash().entries(key);
}
// 修改商品数量
public void updateQuantity(Long userId, Long productId, Integer quantity) {
String key = CART_KEY_PREFIX + userId;
if (quantity <= 0) {
redisTemplate.opsForHash().delete(key, productId.toString());
} else {
redisTemplate.opsForHash().put(key, productId.toString(), quantity);
}
}
}
四、List(列表)
4.1 底层实现
# 列表操作方向
LPUSH queue "a" # 左侧插入
RPUSH queue "b" # 右侧插入
LPOP queue # 左侧弹出
RPOP queue # 右侧弹出
4.2 常用命令
# 插入操作
LPUSH mylist "a" "b" "c" # 返回: 3
RPUSH mylist "x" "y" "z" # 返回: 6
# 弹出操作
LPOP mylist # 返回: "c"
RPOP mylist # 返回: "z"
# 范围查询
LRANGE mylist 0 -1 # 获取所有元素
LINDEX mylist 2 # 获取索引2的元素
LLEN mylist # 获取长度
# 阻塞操作(消息队列常用)
BLPOP queue1 queue2 10 # 阻塞10秒弹出
BRPOP queue 10
# 修剪
LTRIM mylist 0 99 # 只保留前100个
# 插入中间
LINSERT mylist BEFORE "b" "a0"
LINSERT mylist AFTER "b" "b0"
# 修改
LSET mylist 0 "new_value"
# 删除
LREM mylist 2 "a" # 删除前2个"a"
4.3 应用场景
// 1. 最新消息列表(类似时间线)
@Service
public class TimelineService {
// 发布新消息(左侧插入)
public void postMessage(Long userId, String content) {
String key = "timeline:user:" + userId;
String message = userId + ":" + content;
redisTemplate.opsForList().leftPush(key, message);
// 保留最近1000条
redisTemplate.opsForList().trim(key, 0, 999);
}
// 分页获取消息
public List<String> getMessages(Long userId, int page, int size) {
String key = "timeline:user:" + userId;
int start = (page - 1) * size;
int end = start + size - 1;
return redisTemplate.opsForList().range(key, start, end);
}
}
// 2. 简单消息队列(生产者-消费者)
@Component
public class SimpleMessageQueue {
// 生产者
public void produce(String queue, String message) {
redisTemplate.opsForList().rightPush(queue, message);
}
// 消费者(阻塞)
public String consume(String queue, long timeout, TimeUnit unit) {
return redisTemplate.opsForList().leftPop(queue, timeout, unit);
}
}
五、Set(集合)
5.1 底层实现
5.2 常用命令
# 添加删除
SADD tags "java" "redis" "mysql" # 添加
SREM tags "mysql" # 删除
# 查询
SMEMBERS tags # 获取所有
SISMEMBER tags "java" # 判断是否存在
SCARD tags # 获取数量
# 随机操作
SRANDMEMBER tags 2 # 随机获取2个
SPOP tags 1 # 随机弹出1个
# 集合运算
SADD set1 {1,2,3}
SADD set2 {2,3,4}
SINTER set1 set2 # 交集 {2,3}
SUNION set1 set2 # 并集 {1,2,3,4}
SDIFF set1 set2 # 差集 {1}
# 存储结果
SINTERSTORE result set1 set2
SUNIONSTORE result set1 set2
SDIFFSTORE result set1 set2
5.3 应用场景
// 1. 用户标签系统
@Service
public class TagService {
// 给用户打标签
public void addTags(Long userId, String... tags) {
String key = "user:tags:" + userId;
redisTemplate.opsForSet().add(key, tags);
}
// 获取共同标签(好友匹配)
public Set<String> getCommonTags(Long userId1, Long userId2) {
String key1 = "user:tags:" + userId1;
String key2 = "user:tags:" + userId2;
return redisTemplate.opsForSet().intersect(key1, key2);
}
// 推荐相似用户(基于标签交集数量)
public List<Long> recommendUsers(Long userId, int limit) {
String userKey = "user:tags:" + userId;
// 遍历其他用户,计算交集大小
// ... 排序返回
}
}
// 2. 抽奖系统
@Component
public class LotteryService {
// 参与抽奖
public void join(Long userId, String activityId) {
String key = "lottery:" + activityId;
redisTemplate.opsForSet().add(key, userId.toString());
}
// 随机抽取
public Long draw(String activityId) {
String key = "lottery:" + activityId;
String winner = (String) redisTemplate.opsForSet().pop(key);
return winner != null ? Long.valueOf(winner) : null;
}
// 抽取多名
public List<Long> drawMultiple(String activityId, int count) {
String key = "lottery:" + activityId;
List<String> winners = redisTemplate.opsForSet().pop(key, count);
return winners.stream().map(Long::valueOf).collect(Collectors.toList());
}
}
六、ZSet(有序集合)
6.1 底层实现
6.2 常用命令
# 添加/更新
ZADD ranking 100 "player1" 95 "player2" 88 "player3"
# 查询分数
ZSCORE ranking "player1" # 返回100
ZRANK ranking "player1" # 返回0(排名,升序)
ZREVRANK ranking "player1" # 返回2(降序排名)
# 范围查询(按排名)
ZRANGE ranking 0 -1 # 升序全部
ZRANGE ranking 0 -1 WITHSCORES # 带分数
ZREVRANGE ranking 0 2 # 前三名
# 范围查询(按分数)
ZRANGEBYSCORE ranking 90 100 # 分数90-100
ZREVRANGEBYSCORE ranking 100 90 # 反向
ZCOUNT ranking 80 100 # 计数
# 增加分数
ZINCRBY ranking 5 "player1" # player1分数+5
# 删除
ZREM ranking "player1"
ZREMRANGEBYRANK ranking 0 10 # 删除0-10名
ZREMRANGEBYSCORE ranking 0 60 # 删除60分以下
# 集合运算
ZUNIONSTORE result 2 zset1 zset2 WEIGHTS 1 2
ZINTERSTORE result 2 zset1 zset2
6.3 应用场景
// 1. 排行榜系统
@Service
public class RankService {
// 更新分数
public void updateScore(String leaderboard, String userId, double score) {
String key = "rank:" + leaderboard;
redisTemplate.opsForZSet().add(key, userId, score);
}
// 获取前三名
public List<RankItem> getTop3(String leaderboard) {
String key = "rank:" + leaderboard;
Set<ZSetOperations.TypedTuple<String>> top3 =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, 2);
return top3.stream()
.map(tuple -> new RankItem(tuple.getValue(), tuple.getScore().doubleValue()))
.collect(Collectors.toList());
}
// 获取用户排名
public Long getUserRank(String leaderboard, String userId) {
String key = "rank:" + leaderboard;
Long rank = redisTemplate.opsForZSet().reverseRank(key, userId);
return rank != null ? rank + 1 : null;
}
// 获取附近分数段用户
public Set<String> getNearbyUsers(String leaderboard, double score, int range) {
String key = "rank:" + leaderboard;
return redisTemplate.opsForZSet()
.rangeByScore(key, score - range, score + range);
}
}
// 2. 延时队列(时间戳作为score)
@Component
public class DelayQueue {
// 添加延时任务
public void addTask(String queue, String task, long delayMillis) {
long executeTime = System.currentTimeMillis() + delayMillis;
String key = "delay:" + queue;
redisTemplate.opsForZSet().add(key, task, executeTime);
}
// 获取到期任务
public List<String> getExpiredTasks(String queue) {
String key = "delay:" + queue;
long now = System.currentTimeMillis();
// 获取所有到期任务
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore(key, 0, now);
if (tasks != null && !tasks.isEmpty()) {
// 删除已获取的任务
redisTemplate.opsForZSet().removeRangeByScore(key, 0, now);
}
return tasks != null ? new ArrayList<>(tasks) : Collections.emptyList();
}
}
七、高级数据类型
7.1 Bitmap(位图)
# 设置位
SETBIT sign:user:1001 0 1 # 第0天签到
SETBIT sign:user:1001 1 1 # 第1天签到
# 获取位
GETBIT sign:user:1001 0 # 返回1
# 统计
BITCOUNT sign:user:1001 # 统计签到的天数
# 位运算
BITOP AND result key1 key2 # AND运算
BITOP OR result key1 key2 # OR运算
// 签到系统
@Component
public class SignService {
// 用户签到
public boolean sign(Long userId, int dayOfYear) {
String key = "sign:" + userId + ":" + LocalDate.now().getYear();
return redisTemplate.opsForValue().setBit(key, dayOfYear - 1, true);
}
// 获取本月签到天数
public long getMonthSignCount(Long userId, YearMonth yearMonth) {
String key = "sign:" + userId + ":" + yearMonth.getYear();
int days = yearMonth.lengthOfMonth();
// 需要遍历计算,或用BITCOUNT
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitCount(key.getBytes()));
}
}
7.2 HyperLogLog(基数统计)
# 添加元素
PFADD uv:2024-01-01 "user1" "user2" "user3"
# 统计
PFCOUNT uv:2024-01-01 # 返回3
# 合并
PFMERGE uv:week uv:mon uv:tue uv:wed
// UV统计
@Service
public class UVService {
// 记录用户访问
public void recordAccess(String page, Long userId, LocalDate date) {
String key = "uv:" + page + ":" + date;
redisTemplate.opsForHyperLogLog().add(key, userId.toString());
}
// 获取UV数(误差约0.81%)
public Long getUV(String page, LocalDate date) {
String key = "uv:" + page + ":" + date;
return redisTemplate.opsForHyperLogLog().size(key);
}
}
7.3 Geo(地理位置)
# 添加位置
GEOADD cities 116.397128 39.916527 "Beijing"
GEOADD cities 121.480237 31.236305 "Shanghai"
# 计算距离
GEODIST cities Beijing Shanghai km # 约1067km
# 查找附近
GEORADIUS cities 116.0 39.0 200 km
GEORADIUSBYMEMBER cities Beijing 200 km
// 附近的人
@Service
public class GeoService {
// 上报位置
public void reportLocation(Long userId, double lng, double lat) {
String key = "location:nearby";
redisTemplate.opsForGeo().add(key, new Point(lng, lat), userId.toString());
}
// 查找附近的人
public List<NearbyUser> findNearby(double lng, double lat, double radius) {
String key = "location:nearby";
Circle circle = new Circle(new Point(lng, lat), new Distance(radius, Metrics.KILOMETERS));
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(key, circle);
return results.getContent().stream()
.map(result -> new NearbyUser(
result.getContent().getName(),
result.getDistance().getValue()
))
.collect(Collectors.toList());
}
}
7.4 Stream(消息流)
# 添加消息
XADD mystream * name "John" age 30
# 读取消息
XRANGE mystream - + # 读取所有
XREAD COUNT 10 STREAMS mystream 0 # 从头读
# 消费者组
XGROUP CREATE mystream mygroup 0
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
八、数据类型选择决策树
九、内存优化建议
| 数据类型 | 优化建议 |
|---|---|
| String | 使用整数编码,避免存储过大JSON |
| Hash | 控制字段数,使用ziplist编码 |
| List | 使用quicklist,控制单个节点大小 |
| Set | 纯整数用intset |
| ZSet | 小数据量用ziplist |
十、面试高频问题
Q1:Redis为什么这么快?
纯内存+单线程+高效数据结构+I/O多路复用(见第15篇)
Q2:ZSet底层为什么用跳表而不是红黑树?
跳表实现简单、支持范围查询更高效、并发友好
Q3:Hash什么时候用ziplist?
字段数<512且每个字段大小<64字节
Q4:String最大能存多大?
512MB
Q5:如何选择合适的数据类型?
根据数据结构和访问模式选择(见决策树)
十一、总结
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 缓存对象 | Hash | 可单独操作字段 |
| 计数器 | String | INCR原子操作 |
| 排行榜 | ZSet | 自动排序 |
| 去重 | Set | 自动去重 |
| 消息队列 | Stream | 支持消费组 |
| UV统计 | HyperLogLog | 内存占用小 |
如果觉得本文对你有帮助,欢迎点赞、收藏、评论三连支持!

|
🌺The End🌺点点关注,收藏不迷路🌺
|
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)