文章目錄
- 前言
- redis中的set結構
- 疑問1 :為什么使用數組后 整體時間復雜度還是O(1)
- 疑問2: set特性是無序的那為什么當元素少的時候 用連續數組 去存儲呢?
- 疑問3:當元素少于512的時候即使用intset存儲的時候 是如何維護唯一性的?
- 疑問4: set底層的hashtable 如何處理hash沖突的 對比redis 的Hash類型的處理策略
- 實際看看 intset 和hashtable 的底層存儲數據區別
- 使用例子
- 在線用戶統計(去重)
- 相似度計算(用戶標簽交集)
- 秒殺活動參與者去重
- 總結
前言
我們已經分析了三種不同數據類型 在redis中的使用 以及底層的數據結構
Redis常用數據結構以及多并發場景下的使用分析:String類型
Redis常用數據結構以及多并發場景下的使用分析:Hash類型
Redis常用數據結構以及多并發場景下的使用分析:List類型
接下來 我們要來分析下 set的基本使用
redis中的set結構
按照慣例首先還是需要搞清楚 redis中的 set 結構是怎樣去處理的
Redis 中 Set 類型的兩種底層實現方式:intset(整數集合) 和 hashtable(哈希表)
查找、插入、刪除時間復雜度為O(1)
intset(整數集合)
所有元素都是整數(可以轉為 long 類型);元素個數小于 set-max-intset-entries(默認 512);
特點:
連續內存:底層是一個連續的數組(有序數組 優勢利用上了 內存的局部性 所以是連續內存 )
查找:使用二分查找(因為數組是有序的)
插入:時保持有序(需要移動數組)
hashtable(哈希表)
元素中存在非整數(如字符串、UUID、中文等);或者元素數量超過 set-max-intset-entries(默認512);或者手動添加一個大字符串,也會觸發升級。
特點:
哈希表結構:底層是一個 dict;插入查找效率 O(1):基于哈希函數定位;空間占用較大:因為要維護哈希桶和沖突處理結構;支持任意類型元素(比如 "abc", "hello", "uuid");
看完基本介紹 不知道你是否有以下的疑問?
疑問1 :為什么使用數組后 整體時間復雜度還是O(1)
前面說了intset是一個內存連續 并且內部integer 連續的數組 采用二分查找 我們都只帶二分查找時間復雜度是O(logn)而不是O(1) 為什么還是O(1)呢?
解答:
那就是因為 集合較小(只有512個元素),在實踐中表現為“常數時間”—— 所以平均復雜度可認為是 O(1)。
疑問2: set特性是無序的那為什么當元素少的時候 用連續數組 去存儲呢?
解答:
高效性 + 空間緊湊性。
內存連續性 為了 利用上 CPU Cache 緩存的機制 盡管查找用的是 二分查找 O(log?n),但數據小、內存局部性好,速度非常快
疑問3:當元素少于512的時候即使用intset存儲的時候 是如何維護唯一性的?
解答:
先二分查找,發現已經存在元素就不插入
疑問4: set底層的hashtable 如何處理hash沖突的 對比redis 的Hash類型的處理策略
Set 類型:
Set 的底層是 dict,插入元素之前 Redis 會先判斷:
當前 key 是否已經存在于某個 hash 桶的鏈表中(即元素是否存在)
插入邏輯如下:
if (dictFind(set, value) == NULL) {dictAdd(set, value); // 插入
}
如果哈希沖突,但值不相等(鏈表中找不到值),會繼續插入;
如果值已經存在(即鏈表中有相同元素),則不插入,保持唯一性。
結論:
Set 類型允許哈希沖突,只要值不同就能插入;不允許值重復,即使哈希值一樣,Redis 也會用 memcmp 判斷值是否相等
Hash 類型(field-value 映射):
Hash 類型本質是:
dict<field, value>
與 Set 類似:
沖突時使用鏈式哈希解決
同一個 field 不允許重復,如果 field 已存在,則更新 value
插入邏輯:
dictReplace(hash, field, value); // 如果存在就覆蓋
總結:
Set:
bucket[5] --> ["apple"] --> ["banana"] // 值不能重復,但可以 hash 沖突Hash:
bucket[5] --> ["name" => "Tom"] --> ["age" => 20] // key 沖突則更新 value
實際看看 intset 和hashtable 的底層存儲數據區別
設計一個實驗去看插入519個數據之前 和 插入520個數據的區別
@Service
@RequiredArgsConstructor
@Slf4j
public class SetEncodingDemoService {private final RedisTemplate<String, String> redisTemplate;// key for testingprivate static final String SET_KEY = "demo:set:encoding";// 清空 & 初始化測試public void runEncodingTest() {redisTemplate.delete(SET_KEY);log.info("開始添加整數元素,觸發 intset -> hashtable 升級演示");// 1. 添加 500 個整數(仍為 intset 編碼)IntStream.range(0, 500).forEach(i -> {redisTemplate.opsForSet().add(SET_KEY, String.valueOf(i));});log.info("添加 500 個整數完成,請用命令 `debug object demo:set:encoding` 查看編碼,預計為 intset");// 2. 繼續添加超過限制,觸發升級IntStream.range(500, 520).forEach(i -> {redisTemplate.opsForSet().add(SET_KEY, String.valueOf(i));});log.info( 添加第 520 個整數完成,預計編碼變為 hashtable");}
}
測試類
@SpringBootTest
class SetEncodingDemoServiceTest {@Autowiredprivate SetEncodingDemoService setEncodingDemoService;@Testpublic void testEncodingChange() {setEncodingDemoService.runEncodingTest();}
}
分別執行步驟 1 和步驟 2:
可以非常詳細的看到 內部數據結構由intset 變成了hashtable
使用例子
okok 我們已經了解了 set底層為整數數組+hashtable 那么接下來我們就來看看實際的使用例子
wait 稍等一下 set 有哪些常見的使用場景
特異元素都多少個? 求交并集?元素是否存在?
ok 那不就是可以使用這些場景嗎
在線用戶統計(去重)
記錄所有在線用戶,使用 Set 去重,并可實時獲取當前在線用戶數量。
redis結構:
// userID加入
SADD online:users userId
// 求在key(online:users)下有多少個userID
SCARD online:users
// 移除online:users userId 元素
SREM online:users userId
@Service
@RequiredArgsConstructor
public class OnlineUserService {private final RedisTemplate<String, Object> redisTemplate;private static final String ONLINE_USER_KEY = "online:users";// 用戶上線public void userOnline(String userId) {redisTemplate.opsForSet().add(ONLINE_USER_KEY, userId);}// 用戶下線public void userOffline(String userId) {redisTemplate.opsForSet().remove(ONLINE_USER_KEY, userId);}// 獲取當前在線人數public long getOnlineCount() {return redisTemplate.opsForSet().size(ONLINE_USER_KEY);}// 獲取所有在線用戶public Set<Object> getAllOnlineUsers() {return redisTemplate.opsForSet().members(ONLINE_USER_KEY);}
}
相似度計算(用戶標簽交集)
為每個用戶打標簽,利用標簽集合之間的交集計算相似度(如推薦系統)。
redis結構
// 給user:tags:uid123 打上三個標簽
SADD user:tags:uid123 "java" "redis" "ai"
// 給user:tags:uid456 打上三個標簽
SADD user:tags:uid456 "java" "kafka" "ai"
// 求交集
SINTER user:tags:uid123 user:tags:uid456
@Service
@RequiredArgsConstructor
public class UserTagSimilarityService {private final RedisTemplate<String, Object> redisTemplate;// 為用戶添加標簽public void addTags(String userId, String... tags) {String key = "user:tags:" + userId;redisTemplate.opsForSet().add(key, (Object[]) tags);}// 獲取兩個用戶的共同標簽(交集)public Set<Object> getCommonTags(String uid1, String uid2) {String key1 = "user:tags:" + uid1;String key2 = "user:tags:" + uid2;return redisTemplate.opsForSet().intersect(key1, key2);}// 相似度得分(交集大小 / 并集大小)public double computeSimilarity(String uid1, String uid2) {String k1 = "user:tags:" + uid1;String k2 = "user:tags:" + uid2;Set<Object> inter = redisTemplate.opsForSet().intersect(k1, k2);Set<Object> union = redisTemplate.opsForSet().union(k1, k2);if (union == null || union.isEmpty()) return 0.0;return (double) inter.size() / union.size();}
}
秒殺活動參與者去重
高并發下秒殺活動記錄用戶參與,每個用戶只能參與一次。
Redis 結構:
// seckill:activity:10001 下 增加一個 userId
SADD seckill:activity:10001 userId
@Service
@RequiredArgsConstructor
public class SeckillService {private final RedisTemplate<String, Object> redisTemplate;// 秒殺參與:返回是否成功(是否重復)public boolean tryJoinSeckill(String activityId, String userId) {String key = "seckill:activity:" + activityId;Long added = redisTemplate.opsForSet().add(key, userId);return added != null && added > 0;}// 獲取參與人數public long getJoinCount(String activityId) {return redisTemplate.opsForSet().size("seckill:activity:" + activityId);}// 判斷用戶是否參與過public boolean hasJoined(String activityId, String userId) {return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember("seckill:activity:" + activityId, userId));}
}
總結:
場景 | Redis Key 樣例 | 操作 | 高并發優勢 |
---|---|---|---|
在線用戶統計 | online:users | SADD / SCARD | 去重 + 實時查詢人數 |
用戶相似度分析 | user:tags:uid123 | SINTER / SUNION | 集合交集操作原子且高效 |
秒殺用戶去重 | seckill:activity:10001 | SADD | 去重保證每人只能參與一次 |
總結
Set類型的并發優勢:
原子性:所有Set操作都是原子的
去重特性:自動處理重復數據
高性能:O(1)時間復雜度的添加/刪除/查詢
集合運算:支持交集、并集、差集等復雜操作
內存高效:緊湊的數據結構