Skip to content

Commit 0c13a7a

Browse files
committed
post: py to go
1 parent 7efac3c commit 0c13a7a

File tree

30 files changed

+1611
-104
lines changed

30 files changed

+1611
-104
lines changed

content/posts/2025-08-24_redis-learn-03.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
+++
2-
date = '2025-08-23T8:00:00+08:00'
2+
date = '2025-08-24T8:00:00+08:00'
33
draft = false
44
title = 'Redis List'
55
tags = ['Redis', 'Database']
Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
+++
2+
date = '2025-08-26T8:00:00+08:00'
3+
draft = false
4+
title = 'Redis Set'
5+
tags = ['Redis', 'Database']
6+
+++
7+
8+
Redis 的集和 set 键允许用户将任意多个不同的元素存储到集和中, 既可以是文本数据, 也可以是二进制数据. 其与列表有以下两个明显的区别:
9+
- 列表可以存储重复元素, 而集和只存储非重复元素
10+
- 列表以有序方式存储元素, 而集和则以无序方式存储元素
11+
12+
下面介绍结合键的各个命令
13+
14+
## Set 集和
15+
16+
- SADD: 将元素添加到集和
17+
```
18+
SADD set element [element ...]
19+
```
20+
返回成功添加的新元素数量作为返回值, 由于集和不存储相同元素, 所以会自动忽略重复的元素
21+
22+
- SREM: 从集和中移出元素
23+
```
24+
SREM set element [element ...]
25+
```
26+
返回被移除的元素数量, 同样的, 不存在的元素会被忽略
27+
28+
- SMOVE: 将元素从一个集和移动到另一个集和
29+
```
30+
SMOVE source target element
31+
```
32+
移动操作成功时返回1, 若不存在于源集和, 返回0.
33+
如果 source 的元素不存在, 则返回0表示失败.
34+
如果 target 的元素已存在, 则会覆盖该元素. 从结果来看, 并不会导致 target 中元素变化, 但是会导致 source 中的该元素消失.
35+
36+
- SMEMBERS: 获取集和包含的所有元素
37+
```
38+
SMEMBERS set
39+
```
40+
由于集和是无序的, 且 SMEMBERS 命令不会进行任何排序操作, 所以根据元素添加的顺序不同, 含相同元素的集和执行该命令结果可能不同.
41+
42+
- SCARD: 获取集和包含的元素数量
43+
```
44+
SCARD set
45+
```
46+
47+
- SISMEMBER: 检查给定元素是否存在于集和
48+
```
49+
SISMEMBER set element
50+
```
51+
返回1表示给定的元素存在于集和中, 返回0表示不存在于集和中.
52+
53+
54+
### 示例: 唯一计数器
55+
例如, 一个网站想要统计浏览量和用户量
56+
- 流览量可以使用是网页被用户访问的次数, 一个用户可以多次访问. 这种类型的数量使用字符串键或者散列键都可以实现
57+
- 用户数量是访问网站的 IP 地址数量, 这时候就需要构建一个更加严格的计数器, 对每个 IP 地址进行一次次数, 这种计数器就是唯一计数器(unique counter)
58+
```
59+
from redis import Redis
60+
61+
class UniqueCounter:
62+
def __init__(self, client, key):
63+
self.client = client
64+
self.key = key
65+
def count_in(self, item):
66+
return self.client.sadd(self.key, item)
67+
def get_result(self):
68+
return self.client.scard(self.key)
69+
70+
client = Redis(decode_responses=True)
71+
counter = UniqueCounter(client, "ip counter")
72+
print("Add ip", counter.count_in("8.8.8.8"))
73+
print("Add ip", counter.count_in("9.9.9.9"))
74+
print("Add ip", counter.count_in("10.10.10.10"))
75+
76+
print("Numbers of IP:", counter.get_result())
77+
```
78+
79+
80+
### 示例: 点赞
81+
点赞功能可以使用集和来实现, 保证了每个用户对同一个内容只能点1次赞
82+
```
83+
from redis import Redis
84+
85+
class Like:
86+
def __init__(self, client, key):
87+
self.client = client
88+
self.key = key
89+
90+
def cast(self, user):
91+
"""执行点赞 True/False"""
92+
return self.client.sadd(self.key, user)
93+
94+
def undo(self, user):
95+
"""取消点赞"""
96+
self.client.srem(self.key, user)
97+
98+
def is_liked(self, user):
99+
"""是否已点赞"""
100+
return self.client.sismember(self.key, user)
101+
102+
def get_all_liked_users(self):
103+
"""所有点赞用户"""
104+
return self.client.smembers(self.key)
105+
106+
def count(self):
107+
"""点赞人数"""
108+
return self.client.scard(self.key)
109+
110+
client = Redis(decode_responses=True)
111+
like_topic = Like(client, "topic::10086::like")
112+
113+
print("Peter like:", like_topic.cast("peter"))
114+
print("Mary like:", like_topic.cast("mary"))
115+
print("Liked Users:", like_topic.get_all_liked_users())
116+
print("How many likes:", like_topic.count())
117+
print("Peter liked:", like_topic.is_liked("peter"))
118+
print("Dan liked:", like_topic.is_liked("dan"))
119+
```
120+
121+
122+
### 示例: 投票
123+
问答网站、文章推荐网、论坛这类注重内容质量的网站上通常会提供投票功能, 用户可以通过投票来支持一项内容或者反对一项内容:
124+
- 支持票越多的文章, 会被网站安排到更显眼的位置, 使得网站的用户快速流览高质量内容.
125+
- 反对票越多的文章, 则会被放到更不明显的位置, 甚至被当作广告隐藏起来, 使得用户可以忽略这些低质量内容.
126+
127+
例如 Stackoverflow 上面会对回答的答案进行投票, 帮助用户发现高质量的问题和答案.
128+
129+
```Python
130+
from redis import Redis
131+
132+
def vote_up_key(vote_target):
133+
"""赞成 vote_target 用户集和 key"""
134+
return vote_target + "::vote_up"
135+
136+
def vote_down_key(vote_target):
137+
"""反对 vote_target 用户集和 key"""
138+
return vote_target + "::vote_down"
139+
140+
class Vote:
141+
def __init__(self, client, vote_target):
142+
self.client = client
143+
self.vote_up_set = vote_up_key(vote_target)
144+
self.vote_down_set = vote_down_key(vote_target)
145+
146+
def is_voted(self, user):
147+
"""检查用户是否已投过票"""
148+
return self.client.sismember(self.vote_up_set, user) or self.client.sismember(self.vote_down_set, user)
149+
150+
def vote_up(self, user):
151+
"""user 投赞成票"""
152+
if self.is_voted(user):
153+
return False
154+
self.client.sadd(self.vote_up_set, user)
155+
return True
156+
157+
def vote_down(self, user):
158+
"""user 投反对票"""
159+
if self.is_voted(user):
160+
return False
161+
self.client.sadd(self.vote_down_set, user)
162+
return True
163+
164+
def undo(self, user):
165+
"""取消用户投票"""
166+
self.client.srem(self.vote_up_set, user)
167+
self.client.srem(self.vote_down_set, user)
168+
169+
def vote_up_count(self):
170+
"""赞成票的数量"""
171+
return self.client.scard(self.vote_up_set)
172+
173+
def get_all_vote_up_users(self):
174+
"""所有投赞成票的用户"""
175+
return self.client.smembers(self.vote_up_set)
176+
177+
def vote_down_count(self):
178+
"""反对票的数量"""
179+
return self.client.scard(self.vote_down_set)
180+
181+
def get_all_vote_down_users(self):
182+
"""所有投反对票的用户"""
183+
return self.client.smembers(self.vote_down_set)
184+
185+
client = Redis(decode_responses=True) # 是否将字节数据自动解码额日字符串
186+
question_vote = Vote(client, "question::10")
187+
print("Peter 投支持票:", question_vote.vote_up("peter"))
188+
print("Jack 投支持票:", question_vote.vote_up("jack"))
189+
print("Tom 投支持票:", question_vote.vote_up("tom"))
190+
print("Mary 投反对票:", question_vote.vote_down("mary"))
191+
192+
print("支持票数量:", question_vote.vote_up_count())
193+
print("反对票数量:", question_vote.vote_down_count())
194+
195+
print("支持票用户:", question_vote.get_all_vote_up_users())
196+
print("反对票用户:", question_vote.get_all_vote_down_users())
197+
198+
# 取消用户投票(为了多次运行代码)
199+
question_vote.undo("peter")
200+
question_vote.undo("jack")
201+
question_vote.undo("tom")
202+
question_vote.undo("mary")
203+
```
204+
205+
### 示例: 社交关系
206+
Twitter 这类社交软件都可以通过关注或者加好友的方式, 构成一种社交关系. 这些网站上的用户都可以关注其他用户, 也可以被其他用户关注. 通过正在关注名单(following list), 用户可以查看自己正在关注的用户及其人数; 通过关注者名单(follower list), 用户可以查看有哪些人正在关注自己.
207+
208+
下面使用集和来维护这种关系:
209+
- 程序为每个用户维护两个集和: 一个集和存储用户的正在关注名单, 另一个集和存储用户的关注者名单.
210+
- 当 A 关注 B 的时候, 将 A 加入自己的 following list, 并加入 B 的follower list.
211+
- 当 A 取消对 B 的关注的时候, 将 A 从自己的 following list 移出, 并将 A 从 B 的 follower list 移除.
212+
213+
```Python
214+
def following_key(user):
215+
return user + "::following"
216+
217+
def follower_key(user):
218+
return user + "::follower"
219+
220+
class Relationship:
221+
def __init__(self, client, user):
222+
self.client = client
223+
self.user = user
224+
225+
def follow(self, target):
226+
"""关注目标用户"""
227+
user_following_set = following_key(self.user)
228+
self.client.sadd(user_following_set, target)
229+
230+
target_follower_set = follower_key(target)
231+
self.client.sadd(target_follower_set, self.user)
232+
233+
def unfollow(self, target):
234+
"""取消关注目标用户"""
235+
user_following_set = following_key(self.user)
236+
self.client.srem(user_following_set, target)
237+
238+
target_follower_set = follower_key(target)
239+
self.client.srem(target_follower_set, self.user)
240+
241+
def is_following(self, target):
242+
"""是否关注了目标用户"""
243+
user_following_set = following_key(self.user)
244+
return self.client.sismember(user_following_set, target)
245+
246+
def get_all_following(self):
247+
"""所有user关注的用户"""
248+
user_following_set = following_key(self.user)
249+
return self.client.smembers(user_following_set)
250+
251+
def get_all_follower(self):
252+
"""所有关注user的用户"""
253+
user_follower_set = follower_key(self.user)
254+
return self.client.smembers(user_follower_set)
255+
256+
def count_following(self):
257+
"""user关注的用户数量"""
258+
user_following_set = following_key(self.user)
259+
return self.client.scard(user_following_set)
260+
261+
def count_follower(self):
262+
"""关注user的用户数量"""
263+
user_follower_set = follower_key(self.user)
264+
return self.client.scard(user_follower_set)
265+
```
266+
267+
- SRANDMEMBER: 随机获取集和中的元素
268+
```
269+
SRANDMEMBER set [count]
270+
```
271+
该命令接受一个可选的 count 参数, 用于指定用户想要获取的元素数量. 默认只返回一个元素.
272+
如果 count 为正数, 将返回 count 个不重复的元素. 当 count 值大于集的元素数量, 将返回集和所有元素.
273+
如果 count 为负数, 则随机返回 abs(count) 个元素, 并且允许出现重复值.
274+
275+
- SPOP: 随机地从集和中移出指定数量的元素
276+
```
277+
SPOP key [count]
278+
```
279+
该命令会返回被移除的元素值作为命令的返回值.
280+
count 参数不同于 SRANDMEMBER 命令的参数, 其值只能为正数
281+
282+
283+
### 示例: 抽奖
284+
为了推销产品并回馈消费者, 商家经常举办一些抽奖活动, 消费者可以抽奖获取礼品. 下面代码展示了使用集和实现的抽象程序, 这个成会把所有参与抽奖的玩家都添加到一个集和中, 然后通过 SRANDMEMBER 命令随机地选出获奖者.
285+
```Python
286+
class Lottery:
287+
def __init__(self, client, key):
288+
self.client = client
289+
self.key = key
290+
291+
def add_player(self, user):
292+
"""添加用户到抽奖名单中"""
293+
self.client.sadd(self.key, user)
294+
295+
def get_all_players(self):
296+
"""返回参加抽奖活动的所有用户"""
297+
return self.client.smembers(self.key)
298+
299+
def player_count(self):
300+
"""返回抽奖用户数量"""
301+
return self.client.scard(self.key)
302+
303+
def draw(self, number):
304+
"""抽取指定数量的获奖者"""
305+
return self.client.srandmember(self.key, number)
306+
```
307+
考虑到完整的抽奖者名单可能会有用, 所以这个抽奖程序使用了随机获取元素的 SRANDMEMBER 命令, 而不是随机移除元素的 SPOP 命令. 如果不需要保留完整的名单, 也可以使用 SPOP 命令实现抽奖程序.
308+
309+
- SINTER、SINTERSTORE: 对集和执行交集计算
310+
```
311+
SINTER set [set ...]
312+
```
313+
该命令计算用户给定的所有集和的交集, 返回交集的所有元素.
314+
此外, 还有 SINTERSTORE 命令, 将集和的交集计算结果存储到指定的键里面.
315+
```
316+
SINTERSTORE destination_key set [set ...]
317+
```
318+
如果给定的键已存在, 则 SINTERSTORE 命令结果会覆盖原来的集和键
319+
320+
- SUNION、SUNIONSTORE: 对集和执行并集计算
321+
```
322+
SUNION set [set ...]
323+
```
324+
并集计算类似上面的交集计算
325+
326+
- SDIFF、SDIFFSTORE: 对集和执行差集计算
327+
```
328+
SDIFF set [set ...]
329+
```
330+
SDIFF 命令会安装用户给定集和的顺序, 从左到右依次对给定的集和执行差集计算.
331+
332+
> 因为对集合执行交集、并集、差集等集合计算需要耗费大量的资源, 所以用户应该尽量使用SINTERSTORE等命令来存储并重用计算结果, 而不要每次都重复进行计算.
333+
> 此外, 当集合计算涉及的元素数量非常大时, Redis服务器在进行计算时可能会被阻塞.
334+
> 这时, 可以考虑使用Redis的复制功能, 通过从服务器来执行集合计算任务, 从而确保主服务器可以继续处理其他客户端发送的命令请求.
335+
336+
- 共同关注与推荐关注
337+
前面使用集和实现了社交网站好友关系的存储, 即关注和被关注列表. 除此之外, 社交网站还通常会提供一些额外功能, 例如共同关注, 推荐关注等.
338+
要实现共同关注功能, 程序需要计算出两个用户正在关注集和之间的交集.
339+
推荐关注可以从用户关注集和中, 随机选出指定数量的用户作为种子用户, 然后对这些用户的正在管组集和执行并集计算, 最后从这个并集中随机选出一些推荐关注的对象.
340+
341+
### 示例: 使用反向索引构建商品筛选器
342+
在访问购物类网站的时候, 通常可以通过一些标签来筛选产品. 这时候, 对每个产品可以建立一个集和, 对每个标签也都建立一个集和, 这样就得到了一份物品到关键字, 以及关键字到物品的映射关系.
343+
```Python
344+
def make_item_key(item):
345+
return "InvertedIndex::" + item + "::keyword"
346+
347+
def make_keyword_key(keyword):
348+
return "InvertedIndex::" + keyword + "::item"
349+
350+
class InvertedIndex:
351+
def __init__(self, client):
352+
self.client = client
353+
354+
def add_index(self, item, *keywords):
355+
"""为物品添加关键字"""
356+
# 将给定物品添加到
357+
item_key = make_item_key(item)
358+
result = self.client.sadd(item_key, *keywords)
359+
# 遍历关键字集和, 将该物品添加进去
360+
for keyword in keywords:
361+
keyword_key = make_keyword_key(keyword)
362+
self.client.sadd(keyword_key, item)
363+
# 返回添加关键字数量作为结果
364+
return result
365+
366+
def remove_index(self, item, *keywords):
367+
"""移除物品的关键字"""
368+
item_key = make_item_key(item)
369+
result = self.client.srem(item_key, *keywords)
370+
for keyword in keywords:
371+
keyword_key = make_keyword_key(keyword)
372+
self.client.srem(keyword_key, item)
373+
return result
374+
375+
def get_keywords(self, item):
376+
"""获取物品所有的关键字"""
377+
return self.client.smembers(make_item_key(item))
378+
379+
def get_items(self, *keywords):
380+
"""根据给定的关键字获取物品"""
381+
# 根据给定的关键字计算出与之对应的集合 key
382+
keyword_key_list = map(make_keyword_key, keywords)
383+
# 将这些集和 key 做并集
384+
return self.client.sinter(*keyword_key_list)
385+
```

0 commit comments

Comments
 (0)