Redis数据结构深度解析:从基础类型到高级应用的完整指南
Redis数据结构概述
Redis不仅仅是一个简单的key-value存储系统,它支持丰富的数据结构,这使得它能够胜任各种复杂的应用场景。理解这些数据结构的特点和底层实现,是用好Redis的关键。
Redis的核心优势
内存存储:所有数据存储在内存中,提供极快的读写速度。
丰富的数据结构:不仅支持基本的字符串,还支持列表、集合、哈希等复杂结构。
原子操作:所有操作都是原子性的,天然支持并发场景。
持久化支持:通过RDB和AOF提供数据持久化能力。
Redis数据结构分类
五种核心数据结构:
- String(字符串)
- Hash(哈希表)
- List(列表)
- Set(集合)
- Zset(有序集合)
其他数据结构:
- Bitmap、HyperLogLog、Geo、Stream等高级数据结构
- Pub/Sub发布订阅功能
String(字符串)数据结构
String是Redis最基础也是最常用的数据结构,但它的功能远比想象中强大。
基本特性
存储内容:
- 文本字符串
- 数字(整数和浮点数)
- 二进制数据(如图片、序列化对象)
最大长度:512MB
常用命令
基础操作:
# 设置和获取
SET key value
GET key
# 批量操作
MSET key1 value1 key2 value2
MGET key1 key2
# 设置过期时间
SET key value EX 3600 # 1小时后过期
SETEX key 3600 value # 等价写法
# 条件设置
SET key value NX # 键不存在时才设置
SET key value XX # 键存在时才设置
数值操作:
# 自增自减
INCR counter # +1
DECR counter # -1
INCRBY counter 5 # +5
DECRBY counter 3 # -3
INCRBYFLOAT price 0.1 # 浮点数增加
# 示例:实现分布式ID生成
INCR global:user:id # 返回递增的用户ID
字符串操作:
# 追加内容
APPEND key " world" # 在值后追加内容
# 获取长度
STRLEN key
# 获取子串
GETRANGE key 0 4 # 获取前5个字符
SETRANGE key 6 "Redis" # 从位置6开始替换
底层实现原理
SDS(Simple Dynamic String): Redis使用自己实现的SDS而不是C语言原生字符串,主要优势:
struct sdshdr {
int len; // 字符串长度
int free; // 未使用空间长度
char buf[]; // 字符数组
};
SDS的优势:
- O(1)时间复杂度获取长度:直接读取len字段
- 缓冲区溢出保护:操作前检查空间是否足够
- 减少内存重分配:预分配策略减少分配次数
- 二进制安全:可以存储任意二进制数据
内存分配策略:
- 当SDS长度小于1MB时,分配与现有长度相等的未使用空间
- 当SDS长度大于等于1MB时,分配1MB的未使用空间
使用场景
1. 缓存
// Java示例:缓存用户信息
@Service
public class UserService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public User getUserById(Long userId) {
String key = "user:" + userId;
String userJson = redisTemplate.opsForValue().get(key);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 从数据库查询
User user = userRepository.findById(userId);
if (user != null) {
// 缓存1小时
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
Duration.ofHours(1));
}
return user;
}
}
2. 计数器
# 网站访问统计
INCR site:pv:20240410
INCR user:123:login:count
# 限流实现
SET api:user:123:limit 100 EX 3600 # 每小时限制100次
DECR api:user:123:limit
3. 分布式锁
# 获取锁
SET lock:resource:123 "client-id" NX EX 30
# 释放锁(使用Lua脚本保证原子性)
# if redis.call("get", KEYS[1]) == ARGV[1] then
# return redis.call("del", KEYS[1])
# else
# return 0
# end
4. Session存储
# 存储用户会话
SET session:abc123def456 '{"userId":123,"username":"john","loginTime":1712745600}' EX 1800
Hash(哈希表)数据结构
Hash结构类似于编程语言中的Map或Dictionary,非常适合存储对象。
基本特性
结构:key -> {field1: value1, field2: value2, …}
优势:
- 节省内存(相比多个String key)
- 逻辑清晰(相关字段组织在一起)
- 操作灵活(可以单独操作某个字段)
常用命令
基础操作:
# 设置字段
HSET user:123 name "John" age 25 email "john@example.com"
# 获取字段
HGET user:123 name
HMGET user:123 name age # 批量获取
# 获取所有字段
HGETALL user:123
# 删除字段
HDEL user:123 email
# 检查字段是否存在
HEXISTS user:123 name
# 获取字段数量
HLEN user:123
# 获取所有字段名或值
HKEYS user:123
HVALS user:123
数值操作:
# 数值字段自增
HINCRBY user:123 age 1
HINCRBYFLOAT user:123 balance 10.5
# 条件设置
HSETNX user:123 created_time "2024-04-10" # 字段不存在时才设置
底层实现原理
Redis Hash有两种底层实现,会根据数据量自动切换:
1. ZipList(压缩列表) 当同时满足以下条件时使用:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于512个
# ZipList结构示例(内存紧凑)
[zlbytes][zltail][zllen][entry1][entry2]...[entryN][zlend]
优点:内存使用效率高 缺点:查找时间复杂度为O(n)
2. HashTable(哈希表) 当不满足ZipList条件时,自动转换为HashTable:
typedef struct dict {
dictht ht[2]; // 两个哈希表,用于rehash
long rehashidx; // rehash进度,-1表示未进行
// ...
} dict;
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask;// 哈希表大小掩码
unsigned long used; // 已有节点数量
} dictht;
渐进式rehash: 为避免rehash时的性能问题,Redis采用渐进式rehash:
- 为ht[1]分配空间
- 将ht[0]中的数据逐步迁移到ht[1]
- 迁移完成后,ht[1]成为ht[0]
使用场景
1. 用户信息存储
// 存储用户信息
@Service
public class UserProfileService {
public void saveUserProfile(Long userId, UserProfile profile) {
String key = "user:profile:" + userId;
Map<String, String> profileMap = new HashMap<>();
profileMap.put("name", profile.getName());
profileMap.put("age", String.valueOf(profile.getAge()));
profileMap.put("email", profile.getEmail());
profileMap.put("phone", profile.getPhone());
redisTemplate.opsForHash().putAll(key, profileMap);
redisTemplate.expire(key, Duration.ofHours(24));
}
public UserProfile getUserProfile(Long userId) {
String key = "user:profile:" + userId;
Map<Object, Object> profileMap = redisTemplate.opsForHash().entries(key);
if (profileMap.isEmpty()) {
return null;
}
UserProfile profile = new UserProfile();
profile.setName((String) profileMap.get("name"));
profile.setAge(Integer.parseInt((String) profileMap.get("age")));
profile.setEmail((String) profileMap.get("email"));
profile.setPhone((String) profileMap.get("phone"));
return profile;
}
}
2. 购物车实现
# 添加商品到购物车
HSET cart:user:123 product:456 2 # 商品456,数量2
HSET cart:user:123 product:789 1 # 商品789,数量1
# 获取购物车所有商品
HGETALL cart:user:123
# 更新商品数量
HINCRBY cart:user:123 product:456 1 # 数量+1
# 删除商品
HDEL cart:user:123 product:789
3. 统计信息
# 网站统计
HSET stats:20240410 pv 10000 uv 5000 orders 200 revenue 50000.00
# 用户行为统计
HINCRBY user:123:stats login_count 1
HINCRBY user:123:stats page_views 5
List(列表)数据结构
List是一个有序的字符串列表,支持在两端进行插入和删除操作。
基本特性
特点:
- 有序性:元素按插入顺序排列
- 可重复:同一个值可以出现多次
- 双端操作:支持从左右两端操作
最大长度:2^32 - 1个元素
常用命令
插入操作:
# 左端插入
LPUSH mylist "world" "hello" # 结果:["hello", "world"]
# 右端插入
RPUSH mylist "!" # 结果:["hello", "world", "!"]
# 指定位置插入
LINSERT mylist BEFORE "world" "beautiful" # 在"world"前插入
# 设置指定位置的值
LSET mylist 0 "hi" # 设置索引0的值
获取操作:
# 获取指定范围的元素
LRANGE mylist 0 -1 # 获取所有元素
LRANGE mylist 0 2 # 获取前3个元素
# 获取指定位置的元素
LINDEX mylist 0 # 获取第一个元素
# 获取列表长度
LLEN mylist
删除操作:
# 从两端删除
LPOP mylist # 删除并返回第一个元素
RPOP mylist # 删除并返回最后一个元素
# 阻塞式删除(用于实现队列)
BLPOP mylist 10 # 阻塞10秒等待元素
# 删除指定值
LREM mylist 2 "hello" # 删除2个"hello"
# 修剪列表
LTRIM mylist 0 99 # 只保留前100个元素
底层实现原理
Redis List有两种底层实现:
1. QuickList(快速列表) Redis 3.2版本后的默认实现,结合了双向链表和压缩列表的优点:
typedef struct quicklist {
quicklistNode *head; // 头节点
quicklistNode *tail; // 尾节点
unsigned long count; // 总元素个数
unsigned long len; // quicklistNode节点个数
int fill : 16; // 单个节点的填充因子
unsigned int compress : 16; // 压缩深度
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 前驱节点
struct quicklistNode *next; // 后继节点
unsigned char *zl; // ZipList或LZF压缩数据
unsigned int sz; // ziplist的字节数
unsigned int count : 16; // ziplist中的元素个数
// ...
} quicklistNode;
QuickList的优势:
- 双向链表提供O(1)的头尾操作
- ZipList提供内存效率
- 可配置的压缩策略节省内存
配置参数:
# 单个ziplist的最大字节数或元素个数
list-max-ziplist-size -2
# 压缩深度,0表示不压缩
list-compress-depth 0
使用场景
1. 消息队列
// 生产者
@Service
public class MessageProducer {
public void sendMessage(String queue, String message) {
// 从右端插入,实现FIFO队列
redisTemplate.opsForList().rightPush(queue, message);
}
}
// 消费者
@Service
public class MessageConsumer {
public String consumeMessage(String queue, int timeout) {
// 从左端阻塞式获取,实现可靠消费
List<String> result = redisTemplate.opsForList()
.leftPop(queue, Duration.ofSeconds(timeout));
return result.isEmpty() ? null : result.get(0);
}
}
2. 最新动态列表
# 用户发布动态
LPUSH user:123:timeline "发布了一条新动态"
# 获取最新10条动态
LRANGE user:123:timeline 0 9
# 限制动态数量(只保留最新1000条)
LTRIM user:123:timeline 0 999
3. 栈实现
# 压栈
LPUSH stack:user:123 "page1" "page2" "page3"
# 弹栈
LPOP stack:user:123
Set(集合)数据结构
Set是一个无序的字符串集合,元素唯一,支持集合运算。
基本特性
特点:
- 无序性:元素没有固定顺序
- 唯一性:不允许重复元素
- 集合运算:支持并集、交集、差集
常用命令
基础操作:
# 添加元素
SADD myset "apple" "banana" "orange"
# 删除元素
SREM myset "banana"
# 获取所有元素
SMEMBERS myset
# 随机获取元素
SRANDMEMBER myset 2 # 随机获取2个元素
SPOP myset # 随机删除并返回一个元素
# 检查元素是否存在
SISMEMBER myset "apple"
# 获取集合大小
SCARD myset
集合运算:
# 创建测试集合
SADD set1 "a" "b" "c" "d"
SADD set2 "c" "d" "e" "f"
# 交集
SINTER set1 set2 # 结果:["c", "d"]
SINTERSTORE result set1 set2 # 将交集存储到result集合
# 并集
SUNION set1 set2 # 结果:["a", "b", "c", "d", "e", "f"]
SUNIONSTORE result set1 set2
# 差集
SDIFF set1 set2 # 结果:["a", "b"]
SDIFFSTORE result set1 set2
底层实现原理
Redis Set有两种底层实现:
1. IntSet(整数集合) 当集合只包含整数且元素数量较少时使用:
typedef struct intset {
uint32_t encoding; // 编码类型
uint32_t length; // 元素个数
int8_t contents[]; // 元素数组
} intset;
编码类型:
- INTSET_ENC_INT16:16位整数
- INTSET_ENC_INT32:32位整数
- INTSET_ENC_INT64:64位整数
升级机制:当添加的元素超出当前编码范围时,会升级编码类型。
2. HashTable(哈希表) 当不满足IntSet条件时使用HashTable,只使用键,值设为NULL。
使用场景
1. 标签系统
# 给文章添加标签
SADD article:123:tags "Redis" "NoSQL" "Database" "Cache"
# 获取文章标签
SMEMBERS article:123:tags
# 查找有特定标签的文章
SISMEMBER article:123:tags "Redis"
# 统计标签数量
SCARD article:123:tags
2. 去重
// 统计独立访客
@Service
public class VisitorService {
public void recordVisitor(String date, String userId) {
String key = "visitors:" + date;
redisTemplate.opsForSet().add(key, userId);
// 设置过期时间
redisTemplate.expire(key, Duration.ofDays(7));
}
public long getUniqueVisitors(String date) {
String key = "visitors:" + date;
return redisTemplate.opsForSet().size(key);
}
public Set<String> getCommonVisitors(String date1, String date2) {
String key1 = "visitors:" + date1;
String key2 = "visitors:" + date2;
return redisTemplate.opsForSet().intersect(key1, key2);
}
}
3. 权限管理
# 用户权限
SADD user:123:permissions "read" "write" "delete"
# 角色权限
SADD role:admin:permissions "read" "write" "delete" "admin"
# 检查用户是否有某权限
SISMEMBER user:123:permissions "write"
# 获取用户和角色的共同权限
SINTER user:123:permissions role:admin:permissions
Zset(有序集合)数据结构
Zset(Sorted Set)结合了Set和List的特点,既保证元素唯一性,又维护有序性。
基本特性
特点:
- 有序性:根据score(分数)排序
- 唯一性:元素不重复
- 可更新:可以更新元素的分数
应用:排行榜、优先队列、延时队列等
常用命令
基础操作:
# 添加元素
ZADD leaderboard 100 "alice" 200 "bob" 150 "charlie"
# 获取指定范围的元素(按分数排序)
ZRANGE leaderboard 0 -1 # 所有元素,分数从低到高
ZRANGE leaderboard 0 -1 WITHSCORES # 包含分数
ZREVRANGE leaderboard 0 2 # 前3名(分数从高到低)
# 按分数范围获取
ZRANGEBYSCORE leaderboard 100 200 # 分数在100-200之间
ZREVRANGEBYSCORE leaderboard 200 100 # 分数在200-100之间(降序)
# 获取元素排名
ZRANK leaderboard "alice" # 升序排名(从0开始)
ZREVRANK leaderboard "alice" # 降序排名
# 获取元素分数
ZSCORE leaderboard "alice"
# 获取集合大小
ZCARD leaderboard
# 获取分数范围内的元素个数
ZCOUNT leaderboard 100 200
更新操作:
# 增加分数
ZINCRBY leaderboard 50 "alice" # alice分数+50
# 删除元素
ZREM leaderboard "bob"
# 按排名删除
ZREMRANGEBYRANK leaderboard 0 0 # 删除第一名
# 按分数删除
ZREMRANGEBYSCORE leaderboard 0 100 # 删除分数0-100的元素
集合操作:
# 创建两个有序集合
ZADD zset1 1 "a" 2 "b" 3 "c"
ZADD zset2 2 "b" 3 "c" 4 "d"
# 并集
ZUNIONSTORE result 2 zset1 zset2 # 分数相加
ZUNIONSTORE result 2 zset1 zset2 WEIGHTS 1 2 # 给zset2的分数乘以2
# 交集
ZINTERSTORE result 2 zset1 zset2 # 只保留共同元素
底层实现原理
Zset使用两种数据结构的组合:
1. 跳跃表(Skip List) 用于范围查询和排序操作:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头尾节点
unsigned long length; // 节点数量
int level; // 最大层数
} zskiplist;
typedef struct zskiplistNode {
sds ele; // 元素值
double score; // 分数
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度
} level[]; // 层级数组
} zskiplistNode;
跳跃表的优势:
- 查找时间复杂度:O(log N)
- 支持范围查询
- 实现相对简单
2. 哈希表 用于O(1)时间查找元素:
- key:元素值
- value:分数
为什么不用平衡树:
- 跳跃表实现更简单
- 支持范围查询更高效
- 内存局部性更好
使用场景
1. 排行榜系统
@Service
public class LeaderboardService {
// 更新用户分数
public void updateScore(String userId, double score) {
String key = "game:leaderboard";
redisTemplate.opsForZSet().incrementScore(key, userId, score);
}
// 获取排行榜前N名
public List<UserScore> getTopN(int n) {
String key = "game:leaderboard";
Set<ZSetOperations.TypedTuple<String>> result =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, n - 1);
return result.stream()
.map(tuple -> new UserScore(tuple.getValue(), tuple.getScore()))
.collect(Collectors.toList());
}
// 获取用户排名
public Long getUserRank(String userId) {
String key = "game:leaderboard";
Long rank = redisTemplate.opsForZSet().reverseRank(key, userId);
return rank != null ? rank + 1 : null; // 排名从1开始
}
// 获取用户周围的排名
public List<UserScore> getUserNeighbors(String userId, int range) {
String key = "game:leaderboard";
Long rank = redisTemplate.opsForZSet().reverseRank(key, userId);
if (rank == null) return Collections.emptyList();
long start = Math.max(0, rank - range);
long end = rank + range;
Set<ZSetOperations.TypedTuple<String>> result =
redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
return result.stream()
.map(tuple -> new UserScore(tuple.getValue(), tuple.getScore()))
.collect(Collectors.toList());
}
}
2. 延时队列
@Service
public class DelayedQueueService {
// 添加延时任务
public void addDelayedTask(String taskId, String taskData, long delaySeconds) {
String key = "delayed:queue";
long executeTime = System.currentTimeMillis() / 1000 + delaySeconds;
// 使用执行时间作为分数
redisTemplate.opsForZSet().add(key, taskId + ":" + taskData, executeTime);
}
// 获取到期的任务
public List<String> getExpiredTasks(int count) {
String key = "delayed:queue";
long currentTime = System.currentTimeMillis() / 1000;
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore(key, 0, currentTime, 0, count);
// 删除已获取的任务
if (!tasks.isEmpty()) {
redisTemplate.opsForZSet().remove(key, tasks.toArray());
}
return new ArrayList<>(tasks);
}
}
3. 时间序列数据
# 存储用户访问时间
ZADD user:123:visits 1712745600 "page1" 1712745660 "page2" 1712745720 "page3"
# 获取最近1小时的访问
ZRANGEBYSCORE user:123:visits (1712742000 1712745600
# 统计时间范围内的访问次数
ZCOUNT user:123:visits 1712742000 1712745600
其他数据结构简介
除了上述五种核心数据结构,Redis还提供了一些高级数据结构:
Bitmap(位图):
- 基于String实现,用于位操作
- 适合大量布尔值存储,如用户签到、在线状态等
- 主要命令:SETBIT、GETBIT、BITCOUNT、BITOP
HyperLogLog:
- 用于基数统计的概率数据结构
- 极小内存占用(12KB),适合大数据量去重统计
- 主要命令:PFADD、PFCOUNT、PFMERGE
Geo(地理位置):
- 基于Zset实现,存储地理位置信息
- 支持距离计算和范围查询
- 主要命令:GEOADD、GEODIST、GEORADIUS
Stream(流):
- 类似消息队列的数据结构
- 支持消息持久化和消费者组
- 适合构建消息系统
这些高级数据结构在特定场景下非常有用,但日常开发中主要还是使用前面介绍的五种核心数据结构。
性能分析和优化建议
时间复杂度对比
数据结构 | 添加 | 删除 | 查找 | 遍历 |
---|---|---|---|---|
String | O(1) | O(1) | O(1) | - |
Hash | O(1) | O(1) | O(1) | O(n) |
List | O(1) | O(n) | O(n) | O(n) |
Set | O(1) | O(1) | O(1) | O(n) |
Zset | O(log n) | O(log n) | O(1) | O(log n + m) |
内存使用优化
1. 选择合适的数据结构
# 不好的设计:为每个用户属性创建单独的key
SET user:123:name "John"
SET user:123:age "25"
SET user:123:email "john@example.com"
# 好的设计:使用Hash存储相关属性
HSET user:123 name "John" age "25" email "john@example.com"
2. 利用压缩配置
# Redis配置优化
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
3. 设置合理的过期时间
// 避免内存泄漏
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(30));
性能监控
监控关键指标:
# 内存使用情况
INFO memory
# 命令统计
INFO commandstats
# 慢查询日志
SLOWLOG GET 10
# 客户端连接
INFO clients
实际应用最佳实践
1. 缓存设计模式
Cache-Aside模式:
@Service
public class ProductService {
public Product getProduct(Long productId) {
String key = "product:" + productId;
// 1. 先查缓存
String productJson = redisTemplate.opsForValue().get(key);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
// 2. 查数据库
Product product = productRepository.findById(productId);
if (product != null) {
// 3. 写入缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(product),
Duration.ofMinutes(30));
}
return product;
}
public void updateProduct(Product product) {
// 1. 更新数据库
productRepository.save(product);
// 2. 删除缓存
String key = "product:" + product.getId();
redisTemplate.delete(key);
}
}
2. 分布式锁实现
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
public boolean tryLock(String key, String value, long expireTime) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, Duration.ofSeconds(expireTime));
return Boolean.TRUE.equals(result);
}
public boolean unlock(String key, String value) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(UNLOCK_SCRIPT);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(key), value);
return Long.valueOf(1).equals(result);
}
public <T> T executeWithLock(String key, long expireTime,
Supplier<T> supplier) {
String value = UUID.randomUUID().toString();
if (tryLock(key, value, expireTime)) {
try {
return supplier.get();
} finally {
unlock(key, value);
}
} else {
throw new RuntimeException("获取锁失败");
}
}
}
3. 限流实现
@Service
public class RateLimiter {
// 滑动窗口限流
public boolean isAllowed(String key, int limit, int windowSize) {
long now = System.currentTimeMillis();
long windowStart = now - windowSize * 1000L;
String script =
"redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +
"local count = redis.call('zcard', KEYS[1]) " +
"if count < tonumber(ARGV[2]) then " +
" redis.call('zadd', KEYS[1], ARGV[3], ARGV[3]) " +
" redis.call('expire', KEYS[1], ARGV[4]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(key),
String.valueOf(windowStart),
String.valueOf(limit),
String.valueOf(now),
String.valueOf(windowSize));
return Long.valueOf(1).equals(result);
}
// 令牌桶限流
public boolean acquireToken(String key, int capacity, int refillRate) {
String script =
"local key = KEYS[1] " +
"local capacity = tonumber(ARGV[1]) " +
"local tokens = tonumber(ARGV[2]) " +
"local interval = tonumber(ARGV[3]) " +
"local current = redis.call('hmget', key, 'tokens', 'last_refill') " +
"local now = tonumber(ARGV[4]) " +
"local current_tokens = tonumber(current[1]) or capacity " +
"local last_refill = tonumber(current[2]) or now " +
"local elapsed = now - last_refill " +
"local new_tokens = math.min(capacity, current_tokens + math.floor(elapsed / interval) * tokens) " +
"if new_tokens >= 1 then " +
" redis.call('hmset', key, 'tokens', new_tokens - 1, 'last_refill', now) " +
" redis.call('expire', key, 3600) " +
" return 1 " +
"else " +
" redis.call('hmset', key, 'tokens', new_tokens, 'last_refill', now) " +
" redis.call('expire', key, 3600) " +
" return 0 " +
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript,
Collections.singletonList(key),
String.valueOf(capacity),
String.valueOf(refillRate),
String.valueOf(1000), // 1秒的间隔
String.valueOf(System.currentTimeMillis()));
return Long.valueOf(1).equals(result);
}
}
总结
Redis的五种核心数据结构是其强大功能的基础,每种数据结构都有其特定的使用场景:
五种核心数据结构总结:
- String:最基础的类型,适用于缓存、计数器、分布式锁
- Hash:适合存储对象,如用户信息、商品信息
- List:有序列表,适用于消息队列、最新动态、栈
- Set:无序去重集合,适用于标签、权限、去重统计
- Zset:有序集合,适用于排行榜、延时队列、时间序列
使用建议:
- 根据业务场景选择合适的数据结构
- 理解底层实现原理,避免性能陷阱
- 合理设置过期时间,避免内存泄漏
- 使用Redis内置的原子操作保证数据一致性
性能要点:
- String和Hash的O(1)操作效率最高
- List适合头尾操作,避免中间插入删除
- Set的集合运算功能强大
- Zset的排序功能在排行榜场景中不可替代
掌握这五种核心数据结构的特点和使用技巧,就能应对大部分Redis应用场景,构建高性能的缓存系统。