Skip to content

Commit 0bac7c0

Browse files
committed
add post: redis-06 & update post: hexagonal microservice
1 parent 82284c0 commit 0bac7c0

File tree

30 files changed

+869
-123
lines changed

30 files changed

+869
-123
lines changed

content/posts/2025-09-10_hexagonal-microservice.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
+++
22
date = '2025-09-10T8:00:00+08:00'
3-
draft = true
3+
draft = false
44
title = 'Service Implementation Patterns for Microservice'
55
tags = ['Microservice']
66
+++
@@ -25,6 +25,4 @@ tags = ['Microservice']
2525

2626
依赖反转的概念经常同控制反转与依赖注入的概念一同出现, 这些是相关但是不同的概念.
2727

28-
29-
30-
28+
依赖反转原则反转了什么? 这个原则改变了构建软件的思路, 与传统先实现底层细节然后再在其上构建接口的做饭相反, 依赖倒置原则鼓励我们先考虑接口, 然后再针对这些接口实现底层细节.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
+++
2+
date = '2025-09-18T8:00:00+08:00'
3+
draft = false
4+
title = 'Redis HyperLogLog'
5+
tags = ['Redis', 'NoSQL']
6+
+++
7+
8+
之前曾介绍过使用 Redis 集和构建唯一计数器, 并将这个计数器用于计算网站的唯一房客 IP.
9+
虽然使用集和实现唯一计数器可以实现该功能, 但这个方法有一个明显的缺陷:
10+
随着被计数元素的不断增多, 唯一计数器占用的内存也会越来越大; 计数器越多, 他们的体积越大, 这一情况就会越严峻.
11+
12+
以计算唯一访客 IP 为例:
13+
- 存储一个 IPv4 格式的 IP 地址最多需要 15 个字节
14+
- 根据网站的规模不同, 每天出现的唯一 IP 可能会有数十万、数百万个
15+
- 为了记录网站在不同时期的访客, 并进行相关的数据分析, 网站可能需要次序地记录每天的唯一访客 IP 数量
16+
17+
综上, 如果一个网站想要长时间记录访客 IP, 就必须创建多个唯一计数器. 如果访客比较多, 那么它创建的每个唯一计数器都将包含大量元素, 并因此占用相当一部分内存.
18+
19+
为了高效解决计算机唯一访客 IP 数量这类问题, 其中一种方法就是 HyperLogLog.
20+
21+
## HyperLogLog 简介
22+
HyperLogLog 是一个专门为了计算集和的基数而创建的概率算法, 对于一个给定的集和, HyperLogLog 可以计算出这个集合的近似基数:
23+
近似基数并非集和的实际基数, 它可能会比实际的基数大一点或者小一点, 但误差会在一个合理范围内.
24+
因此, 那些不需要知道实际基数的程序就可以把这个近似基数当作集合的基数来使用.
25+
26+
HyperLogLog 的优点在于计算近似基础所需的内存并不会因为集和的大小而改变, 无论集和包含元素有多少个, HyperLogLog 进行计算所需的内存总是固定的, 无论集和包含元素多少个, HyperLogLog 进行计算所需的内存总是固定的, 并且是非常少的.
27+
28+
29+
### PFADD: 对集和元素进行计数
30+
```Redis
31+
PFADD hyperloglog element [element ...]
32+
```
33+
根据给定元素是否已经进行过计数, PFADD 命令可能返回0, 也可能返回1:
34+
- 如果给定的所有元素都已经计数, 那么 PFADD 命令返回 0, 表示 HyperLogLog 计算出是近似基数没有发生变化
35+
- 如果给定的的元素中出现了至少一个没有过进行计数的元素, 导致 HyperLogLog 计算出的近似基数发生了变化, 那么 PFADD 命令返回 1
36+
37+
复杂度: O(N), N 为给定元素数量
38+
39+
40+
### PFCOUNT: 返回集和的近似基数
41+
```Redis
42+
PFCOUNT hyperloglog [hyperloglog ...]
43+
```
44+
使用 PFADD 命令对元素进行计数后, 用户可以通过执行 PFCOUNT 命令来获取 HyperLogLog 为集和计算出的近似基数.
45+
当用户给定的 HyperLogLog 不存在时, PFCOUNT 命令将返回 0 作为结果.
46+
当用户向 PFCOUNT 传入多个 HyperLogLog 时, PFCOUNT 命令将对所有给定的 HyperLogLog 执行并行计算, 然后返回并集计算出的近似基数.
47+
```Redis
48+
PFADD alphabets1 "a" "b" "c"
49+
PFADD alphabets2 "c" "d" "e"
50+
PFCOUNT alphabets1 alphabets2
51+
52+
> (integer) 5
53+
```
54+
55+
上面计算类似于
56+
```Redis
57+
PFADD temp-hyperloglog "a" "b" "c" "c" "d" "e"
58+
PFCOUNT temphyperloglog temp-hyperloglog
59+
60+
> (integer) 5
61+
```
62+
63+
### 示例: 优化唯一计数器
64+
```Python
65+
from redis import Redis
66+
67+
class UniqueCounter:
68+
def __init__(self, client, key):
69+
self.client = client
70+
self.key = key
71+
72+
def count_in(self, item):
73+
"""对给定的元素进行计数"""
74+
self.client.pfadd(self.key, item)
75+
76+
def get_result(self):
77+
"""返回计数器的值"""
78+
return self.client.pfcount(self.key)
79+
80+
client = Redis(decode_responses=True)
81+
counter = UniqueCounter(client, 'unique-ip-counter') # 创建一个唯一 IP 计数器
82+
83+
counter.count_in('1.1.1.1')
84+
counter.count_in('2.2.2.2')
85+
counter.count_in('3.3.3.3')
86+
print(counter.get_result())
87+
88+
counter.count_in('3.3.3.3')
89+
print(counter.get_result())
90+
```
91+
通过使用 HyperLogLog 实现的唯一计数器去取代集和实现的唯一计数器, 可以大副降低存储所需的内存.
92+
93+
### 示例: 检测重复信息
94+
在构建应用程序的过程中, 经常要处理广告等垃圾信息. 而发送者经常会使用不同的帐号, 发送相同的垃圾信息, 所有一种简单的方法就是找出重复 的信息: 如果两个用户发送了完全相同的信息, 那么这些信息很可能就是垃圾短信.
95+
96+
判断两段信息是否相同并不是意见容易的事情, 如果使用一般的比较函数, 那复杂度就会很高: O(N*M), 其中 N 为信息的长度, M 为系统目前已有的信息数量.
97+
98+
为了降低复杂度, 可以使用 HyperLogLog 来记录所有以发送的信息: 每当用户发送一条信息时, 程序就使用 PFADD 命令将这条信息添加到 HyperLogLog 中:
99+
- 如果命令返回 1, 说明这条信息未出现过
100+
- 如果命令返回 0, 说明这条信息已出现过
101+
102+
由于 HyperLogLog 是概率算法, 所以即使信息长度非常长, HyperLogLog判断信息是否重复所需的时间也非常短.
103+
```Python
104+
from redis import Redis
105+
106+
class DuplicateChecker:
107+
def __init__(self, client, key):
108+
self.client = client
109+
self.key = key
110+
111+
def is_duplicated(self, content):
112+
"""在信息重复时返回 True, 未重复时返回 False"""
113+
return self.client.pfadd(self.key, content) == 0
114+
115+
def unique_count(self):
116+
"""返回检测器已经检查过的非重复信息数量"""
117+
return self.client.pfcount(self.key)
118+
119+
client = Redis(decode_responses=True)
120+
checker = DuplicateChecker(client, 'duplicate-message-checker')
121+
122+
# 输入非重复信息
123+
checker.is_duplicated("hello world!")
124+
checker.is_duplicated("good morning!")
125+
checker.is_duplicated("bye bye")
126+
print(checker.unique_count())
127+
128+
checker.is_duplicated("hello world!")
129+
```
130+
131+
132+
### PFMERGE: 计算多个 HyperLogLog 的并集
133+
```Redis
134+
PFMERGE destination hyperloglog [hyperloglog ...]
135+
```
136+
该命令可以对多个给定的 HyperLogLog 执行并行计算, 然后把计算得出的并集 HyperLogLog 保存到指定的键中.
137+
如果指定的键已存在, 则会覆盖原有的键, 执行成功后返回 OK.
138+
HyperLogLog 并集计算的近似基数接近于所有给定 HyperLogLog 的被计数集合的并集基数.
139+
```Redis
140+
PFADD numbers1 128 256 512
141+
PFADD numbers2 128 256 512
142+
PFADD numbers3 128 512 1024
143+
PFMERGE union-numbers numbers1 numbers2 numbers3
144+
145+
> OK
146+
147+
PFCOUNT union-numbers
148+
> (integer) 4
149+
```
150+
151+
#### PFCOUNT 与 PFMERGE
152+
**PFCOUNT** 命令在计算多个 HyperLogLog 的近似基数时会执行以下操作:
153+
1. 在内部调用 PFMERGE 命令, 计算所有给定 HyperLogLog 的并集, 并将这个并集存储到一个临时的 HyperLogLog 中
154+
2. 对临时的 HyperLogLog 执行 PFCOUNT 命令, 得到它的近似基数
155+
3. 删除临时 HyperLogLog
156+
4. 向用户返回之前得到的近似基数
157+
158+
### 示例: 实现每周/月度/年度计数器
159+
通过 PFMERGE 命令可以对多个 HyperLogLog 实现的唯一计数器执行并集计算, 从而实现每周/月度/年度计数器:
160+
- 通过对一周内每天的唯一访客 IP 计数器执行 PFMERGE 命令, 可以计算出那一周的唯一访客 IP 数量
161+
- 通过对一个月每天的唯一访客 IP 计数器执行 PFMERGE 命令, 可以计数器那一个月的唯一访客 IP 数量
162+
- 年度甚至更长时间的唯一访客 IP 数量也可以按照类似的方法计算
163+
164+
```Python
165+
from redis import Redis
166+
167+
168+
class UniqueCounterMerger:
169+
def __init__(self, client):
170+
self.client = client
171+
172+
def merge(self, destination, *hyperloglogs):
173+
self.client.pfmerge(destination, *hyperloglogs)
174+
175+
client = Redis(decode_response=True)
176+
merger = UniqueCounterMerger(client)
177+
178+
# 本周 7 天的计数器排名
179+
counters = [
180+
'unique_ip_counter:8-10',
181+
'unique_ip_counter:8-11',
182+
'unique_ip_counter:8-12',
183+
'unique_ip_counter:8-13',
184+
'unique_ip_counter:8-14',
185+
'unique_ip_counter:8-15',
186+
'unique_ip_counter:8-16',
187+
]
188+
189+
# 计算并存储本周的唯一访客 IP 数量
190+
merger.merger('unique_ip_counter::No_33_week', *counters)
191+
192+
# 去本周的唯一访客 IP 数量
193+
weekly_unique_visitors = client.pfcount('unique_ip_counter::No_33_week')
194+
print(f"本周唯一访客 IP 数量: {weekly_unique_visitors}")
195+
```
196+

public/archives/index.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,13 @@
5151
<span class="max-w-[4rem] md:max-w-none truncate">Home</span></a></li><li class="flex items-center gap-1 md:gap-2 min-w-0"><span class="text-muted-foreground/50 flex-shrink-0"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
5252
</span><span class="text-foreground flex items-center gap-0.5 md:gap-1 font-medium min-w-0 flex-shrink-0"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg>
5353
<span class="max-w-[3rem] md:max-w-none truncate">Archives</span></span></li></ol></nav><header class=mb-8><div class="mb-4 flex items-center gap-3"><svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg><h1 class="text-foreground text-3xl font-bold">Archives</h1></div><p class="text-muted-foreground mb-6">Browse all articles in chronological order and discover what interests you.</p><div class="text-muted-foreground flex items-center gap-4 text-sm"><div class="flex items-center gap-1"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
54-
<span>46 posts total</span></div><div class="flex items-center gap-1"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
55-
<span>Timeline view</span></div></div></header><div class=relative><div class="bg-border absolute top-0 bottom-0 left-4 w-0.5"></div><div class=mb-12><div class="relative mb-8 flex items-center"><div class="bg-primary absolute left-0 z-10 flex h-8 w-8 items-center justify-center rounded-full"><svg class="h-4 w-4 text-primary-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg></div><div class=ml-12><h2 class="text-foreground text-2xl font-bold">2025</h2><p class="text-muted-foreground text-sm">44
56-
posts</p></div></div><div class="relative mb-8"><div class="relative mb-4 flex items-center"><div class="bg-accent border-background absolute left-2 z-10 h-4 w-4 rounded-full border-2"></div><div class=ml-12><h3 class="text-foreground text-lg font-semibold">September 2025</h3><p class="text-muted-foreground text-xs">13
57-
posts</p></div></div><div class="ml-12 space-y-3"><article class="group bg-card border-border hover:bg-accent/50 rounded-lg border p-4 transition-all duration-300"><div class="flex items-center justify-between gap-4"><div class="min-w-0 flex-1"><h4 class="text-foreground group-hover:text-primary mb-3 font-medium transition-colors duration-200"><a href=/posts/morden-javascript-tutorial-part-1-fundamentals-06~10/ class=block>Morden Javascript Tutorial Part 1- Fundamentals :06~10</a></h4><div class="text-muted-foreground flex items-center gap-4 text-xs"><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
54+
<span>48 posts total</span></div><div class="flex items-center gap-1"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
55+
<span>Timeline view</span></div></div></header><div class=relative><div class="bg-border absolute top-0 bottom-0 left-4 w-0.5"></div><div class=mb-12><div class="relative mb-8 flex items-center"><div class="bg-primary absolute left-0 z-10 flex h-8 w-8 items-center justify-center rounded-full"><svg class="h-4 w-4 text-primary-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg></div><div class=ml-12><h2 class="text-foreground text-2xl font-bold">2025</h2><p class="text-muted-foreground text-sm">46
56+
posts</p></div></div><div class="relative mb-8"><div class="relative mb-4 flex items-center"><div class="bg-accent border-background absolute left-2 z-10 h-4 w-4 rounded-full border-2"></div><div class=ml-12><h3 class="text-foreground text-lg font-semibold">September 2025</h3><p class="text-muted-foreground text-xs">15
57+
posts</p></div></div><div class="ml-12 space-y-3"><article class="group bg-card border-border hover:bg-accent/50 rounded-lg border p-4 transition-all duration-300"><div class="flex items-center justify-between gap-4"><div class="min-w-0 flex-1"><h4 class="text-foreground group-hover:text-primary mb-3 font-medium transition-colors duration-200"><a href=/posts/redis-hyperloglog/ class=block>Redis HyperLogLog</a></h4><div class="text-muted-foreground flex items-center gap-4 text-xs"><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
58+
<time datetime=2025-09-18>09-18</time></div><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3A9 9 0 113 12a9 9 0 0118 0z"/></svg>
59+
<span>5
60+
min</span></div></div></div></div></article><article class="group bg-card border-border hover:bg-accent/50 rounded-lg border p-4 transition-all duration-300"><div class="flex items-center justify-between gap-4"><div class="min-w-0 flex-1"><h4 class="text-foreground group-hover:text-primary mb-3 font-medium transition-colors duration-200"><a href=/posts/morden-javascript-tutorial-part-1-fundamentals-06~10/ class=block>Morden Javascript Tutorial Part 1- Fundamentals :06~10</a></h4><div class="text-muted-foreground flex items-center gap-4 text-xs"><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
5861
<time datetime=2025-09-17>09-17</time></div><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3A9 9 0 113 12a9 9 0 0118 0z"/></svg>
5962
<span>8
6063
min</span></div></div></div></div></article><article class="group bg-card border-border hover:bg-accent/50 rounded-lg border p-4 transition-all duration-300"><div class="flex items-center justify-between gap-4"><div class="min-w-0 flex-1"><h4 class="text-foreground group-hover:text-primary mb-3 font-medium transition-colors duration-200"><a href=/posts/morden-javascript-tutorial-part-1-fundamentals-01~05/ class=block>Morden Javascript Tutorial Part 1 - Fundamentals :01~05</a></h4><div class="text-muted-foreground flex items-center gap-4 text-xs"><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
@@ -72,6 +75,9 @@
7275
min</span></div></div></div></div></article><article class="group bg-card border-border hover:bg-accent/50 rounded-lg border p-4 transition-all duration-300"><div class="flex items-center justify-between gap-4"><div class="min-w-0 flex-1"><h4 class="text-foreground group-hover:text-primary mb-3 font-medium transition-colors duration-200"><a href=/posts/the-evolution-of-coding-in-the-ai-era/ class=block>The Evolution of Coding in the AI Era</a></h4><div class="text-muted-foreground flex items-center gap-4 text-xs"><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
7376
<time datetime=2025-09-11>09-11</time></div><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3A9 9 0 113 12a9 9 0 0118 0z"/></svg>
7477
<span>9
78+
min</span></div></div></div></div></article><article class="group bg-card border-border hover:bg-accent/50 rounded-lg border p-4 transition-all duration-300"><div class="flex items-center justify-between gap-4"><div class="min-w-0 flex-1"><h4 class="text-foreground group-hover:text-primary mb-3 font-medium transition-colors duration-200"><a href=/posts/service-implementation-patterns-for-microservice/ class=block>Service Implementation Patterns for Microservice</a></h4><div class="text-muted-foreground flex items-center gap-4 text-xs"><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
79+
<time datetime=2025-09-10>09-10</time></div><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3A9 9 0 113 12a9 9 0 0118 0z"/></svg>
80+
<span>2
7581
min</span></div></div></div></div></article><article class="group bg-card border-border hover:bg-accent/50 rounded-lg border p-4 transition-all duration-300"><div class="flex items-center justify-between gap-4"><div class="min-w-0 flex-1"><h4 class="text-foreground group-hover:text-primary mb-3 font-medium transition-colors duration-200"><a href=/posts/dealing-with-grabage-in-python/ class=block>Dealing With Grabage in Python</a></h4><div class="text-muted-foreground flex items-center gap-4 text-xs"><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5A2 2 0 003 7v12a2 2 0 002 2z"/></svg>
7682
<time datetime=2025-09-08>09-08</time></div><div class="flex items-center gap-1"><svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3A9 9 0 113 12a9 9 0 0118 0z"/></svg>
7783
<span>3

0 commit comments

Comments
 (0)