Redis Set与IntSet编码
Redis 的五种数据结构各有特色,用对了才能发挥它的优势。很多人只用到了 String 和 Hash,却不知道 List、Set、ZSet 在特定场景下更合适。本文从应用场景出发,讲什么时候用什么类型。
一、Set的编码方式
#
1.1 两种编码
| 编码 |
结构 |
适用条件 |
| intset |
整数数组 |
元素都是整数且数量少 |
| hashtable |
哈希表 |
元素非整数或数量多 |
#
1.2 编码转换条件
# 默认配置 set-max-intset-entries 512 # intset最多512个元素
|
转换规则:
- 元素都是整数且数量<=512:使用intset
- 任一条件不满足:转换为hashtable
- 转换单向:intset → hashtable后不回退
二、IntSet详解
#
2.1 结构设计
IntSet是紧凑的整数数组:
typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset;
|
内存布局(存储3个int16整数): ┌──────────┬──────────┬───────────────────────────────┐ │ encoding │ length │ contents │ │ 2字节 │ 4字节 │ 2字节 │ 2字节 │ 2字节 │ │ INT16 │ 3 │ 100 │ 200 │ 300 │ └──────────┴──────────┴───────────────────────────────┘ 总大小:4 + 4 + 2*3 = 14字节
|
#
2.2 编码升级
当插入一个超出当前编码范围的整数时,触发升级:
当前状态:encoding=INT16,存储 [100, 200]
插入 40000(超出int16范围):
升级过程: 1. 将encoding改为INT32 2. 重新分配内存(4 * 3 = 12字节) 3. 从后向前迁移数据(防止覆盖) contents[2] = 40000 (int32) contents[1] = 200 (int32) contents[0] = 100 (int32)
升级后: ┌──────────┬──────────┬───────────────────────────────┐ │ INT32 │ 3 │ 100 │ 200 │ 40000 │ └──────────┴──────────┴───────────────────────────────┘ 总大小:4 + 4 + 4*3 = 20字节
|
升级特点:
- 升级是单向的(int16 → int32 → int64),不会降级
- 升级后不再降级,即使删除大值
- 升级操作O(n),但发生频率低
#
2.3 有序性保证
IntSet始终保持有序:
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) { int min = 0, max = intrev32ifbe(is->length)-1, mid = -1; int64_t cur = -1; while (max >= min) { mid = ((unsigned int)min + (unsigned int)max) >> 1; cur = _intsetGet(is, mid); if (value > cur) { min = mid + 1; } else if (value < cur) { max = mid - 1; } else { break; } } }
|
有序性优势:
- 查找:O(log n) 二分查找
- 去重:天然有序,相等元素相邻
#
2.4 插入操作
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; if (valenc > intrev32ifbe(is->encoding)) { return intsetUpgradeAndAdd(is, value); } if (intsetSearch(is, value, &pos)) { if (success) *success = 0; return is; } is = intsetResize(is, intrev32ifbe(is->length)+1); if (pos < intrev32ifbe(is->length)) intsetMoveTail(is, pos, pos+1); _intsetSet(is, pos, value); is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; }
|
三、Hashtable实现
#
3.1 编码转换
当Set使用hashtable编码时,value存储什么?
dictEntry *entry = dictFind(set->ptr, element);
|
#
3.2 查找过程
robj *setTypeIsMember(robj *subject, robj *value) { long long llval; if (subject->encoding == OBJ_ENCODING_HT) { return dictFind(subject->ptr, value) != NULL; } if (subject->encoding == OBJ_ENCODING_INTSET) { if (isObjectRepresentableAsLongLong(value, &llval) == C_OK) { return intsetFind(subject->ptr, llval); } } return 0; }
|
四、集合运算
#
4.1 SINTER(交集)
void sinterCommand(client *c) { robj **setkeys = c->argv + 1; robj *dstkey = NULL; int minidx = 0; for (int j = 1; j < c->argc - 1; j++) { if (setTypeSize(setkeys[j]) < setTypeSize(setkeys[minidx])) { minidx = j; } } si = setTypeInitIterator(setkeys[minidx]); while (setTypeNext(si, &ele, &elesize) != -1) { for (int j = 1; j < c->argc - 1; j++) { if (j == minidx) continue; if (!setTypeIsMember(setkeys[j], ele)) break; } if (j == c->argc - 1) { addReplyBulkCBuffer(c, ele, elesize); } } }
|
优化策略:
#
4.2 SUNION(并集)
void sunionCommand(client *c) { robj *dstset = setTypeCreate(NULL); for (int j = 1; j < c->argc - 1; j++) { robj *setobj = lookupKeyRead(c->db, setkeys[j]); if (setobj == NULL) continue; si = setTypeInitIterator(setobj); while (setTypeNext(si, &ele, &elesize) != -1) { setTypeAdd(dstset, ele); } } setTypeIterator *di = setTypeInitIterator(dstset); while (setTypeNext(di, &ele, &elesize) != -1) { addReplyBulkCBuffer(c, ele, elesize); } }
|
#
4.3 SDIFF(差集)
void sdiffCommand(client *c) { robj *dstset = setTypeCreate(NULL); robj *setobj = lookupKeyRead(c->db, setkeys[1]); si = setTypeInitIterator(setobj); while (setTypeNext(si, &ele, &elesize) != -1) { for (int j = 2; j < c->argc - 1; j++) { if (setTypeIsMember(setkeys[j], ele)) break; } if (j == c->argc - 1) { setTypeAdd(dstset, ele); } } }
|
五、应用场景
#
5.1 共同好友
@Service public class SocialService { public void addFriend(Long userId, Long friendId) { redis.opsForSet().add("friends:" + userId, String.valueOf(friendId)); redis.opsForSet().add("friends:" + friendId, String.valueOf(userId)); } public Set<String> getCommonFriends(Long userId1, Long userId2) { return redis.opsForSet().intersect("friends:" + userId1, "friends:" + userId2); } public Long getFriendCount(Long userId) { return redis.opsForSet().size("friends:" + userId); } public Boolean isFriend(Long userId, Long friendId) { return redis.opsForSet().isMember("friends:" + userId, String.valueOf(friendId)); } }
|
#
5.2 标签系统
@Service public class TagService { public void tagArticle(Long articleId, List<String> tags) { String key = "article:tags:" + articleId; redis.opsForSet().add(key, tags.toArray(new String[0])); redis.expire(key, 30, TimeUnit.DAYS); } public Set<String> getArticleTags(Long articleId) { return redis.opsForSet().members("article:tags:" + articleId); } public Set<String> getArticlesByTag(String tag) { return redis.opsForSet().members("tag:articles:" + tag); } public Set<String> getArticlesByTags(List<String> tags) { String[] keys = tags.stream() .map(t -> "tag:articles:" + t) .toArray(String[]::new); return redis.opsForSet().intersect(keys); } public void indexArticleTags(Long articleId, List<String> tags) { for (String tag : tags) { redis.opsForSet().add("tag:articles:" + tag, String.valueOf(articleId)); } } }
|
#
5.3 IP黑名单
@Component public class IpBlacklistService { private static final String BLACKLIST_KEY = "blacklist:ip"; public void addToBlacklist(String ip) { redis.opsForSet().add(BLACKLIST_KEY, ip); } public void removeFromBlacklist(String ip) { redis.opsForSet().remove(BLACKLIST_KEY, ip); } public boolean isBlacklisted(String ip) { return Boolean.TRUE.equals(redis.opsForSet().isMember(BLACKLIST_KEY, ip)); } public Set<String> getAllBlacklisted() { return redis.opsForSet().members(BLACKLIST_KEY); } }
|
#
5.4 抽奖/随机选取
@Service public class LotteryService { public void addParticipant(String activityId, String userId) { redis.opsForSet().add("lottery:" + activityId, userId); } public List<String> drawWinners(String activityId, int count) { List<String> winners = new ArrayList<>(); for (int i = 0; i < count; i++) { String winner = redis.opsForSet().pop("lottery:" + activityId); if (winner == null) break; winners.add(winner); } return winners; } public List<String> drawWinnersWithReplacement(String activityId, int count) { return new ArrayList<>(redis.opsForSet() .distinctRandomMembers("lottery:" + activityId, count)); } }
|
六、性能优化
#
6.1 利用IntSet节省内存
for (int i = 0; i < 500; i++) { redis.opsForSet().add("followers:" + userId, String.valueOf(i)); }
redis.opsForSet().add("followers:" + userId, "not_a_number");
|
#
6.2 批量操作
redis.opsForSet().add("set", "a", "b", "c", "d", "e");
List<Boolean> exists = redis.executePipelined((RedisCallback<Object>) connection -> { for (String item : items) { connection.setCommands().sIsMember("set".getBytes(), item.getBytes()); } return null; }).stream() .map(o -> (Boolean) o) .collect(Collectors.toList());
|
#
6.3 大Set分片
public void addToShardSet(String key, String member) { int shard = member.hashCode() % 10; redis.opsForSet().add(key + ":" + shard, member); }
public boolean isInShardSet(String key, String member) { int shard = member.hashCode() % 10; return Boolean.TRUE.equals(redis.opsForSet() .isMember(key + ":" + shard, member)); }
|
七、常见问题
#
7.1 Set最大元素数
理论限制:约40亿(2^32 - 1) 实际限制:内存大小
建议:单个Set不超过10000个元素
|
#
7.2 SMEMBERS的性能
SMEMBERS big:set
SSCAN big:set 0 COUNT 100
|
#
7.3 集合运算的复杂度
SINTER:O(N*M),N是最小Set大小,M是Set数量 SUNION:O(N),N是所有Set元素总数 SDIFF:O(N),N是第一个Set的元素数
建议: - 大Set的交集避免在Redis做,应用层处理 - 或预先计算交集存入新Set
|
八、总结
| 特性 |
IntSet |
Hashtable |
| 结构 |
有序整数数组 |
哈希表 |
| 查找 |
O(log n) |
O(1) |
| 插入 |
O(n) |
O(1) |
| 内存 |
极紧凑 |
有指针开销 |
| 适用 |
小整数集合 |
大集合或字符串 |
Set使用建议:
- 存储整数:尽量让Set使用intset编码
- 控制大小:单个Set不超过10000元素
- SSCAN替代SMEMBERS:避免阻塞
- 集合运算:注意复杂度,大Set在应用层处理
- 去重场景:Set是天然的去重数据结构
核心要点
String:简单的键值对,适合缓存、计数器
Hash:存储对象属性,适合用户信息、配置
List:有序列表,适合消息队列、最新列表
Set:无序去重,适合共同好友、抽奖
ZSet:有序集合,适合排行榜、积分系统
总结
选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。