P109
自动补全在平常业务中随处可见,应该算一种最多见最通用的功能。实际业务场景确定要包括包含子串的状况,其实这在必定程度上转换成了搜索功能,即包含某个子串的串,且优先展现前缀匹配的串。若是仅包含前缀,那么可使用 Trie
树,但在包含其余的状况下,使用数据库/ ES
自己自带查询就足够了。能够按照四种状况(精确匹配、前缀、后缀、包含(也可将后两种融合成包含)),分别查询结果,直至达到数据条数上限或者所有查询完毕。但这种使用方法有缺点:查询次数多、难以分页。不过实际场景中须要补全的状况都只要第一页的数据便可。git
P110
需求: 记录最近联系过的 100 我的名,并支持对输入的串进行自动补全。 P110
github
数据量很小,因此能够在 Redis
中用列表维护最近联系人,而后在内存中进行过滤可自动补全的串。redis
步骤: P111
数据库
LREM
)LPUSH
)LTRIM
)P112
需求: 有不少通信录,每一个通信录中有几千我的(仅包含小写英文字母),尽可能减小 Redis
传输给客户端的数据量,实现前缀自动补全。 P112
服务器
思路: 使用有序集合存储人名,利用有序集合的特性:当成员的分值相同时,将根据成员字符串的二进制顺序进行排序。若是要查找 abc
前缀的字符串,那么实际上就是查找介于 abbz...
以后和 abd
以前的字符串。因此问题转化为:如何找到第一个排在 abc
以前的元素的排名 和 第一个排在 abd
以前的元素的排名。咱们能够构造两个不在有序集合中的字符串 (abb{
, abc{
) 辅助定位,由于 {
是排在 z
后第一个不适用的字符,这样能够保证这两个字符串不存在与有序集合中,且知足转化后的问题的限制。 P113
markdown
综上: 经过将给定前缀的最后一个字符替换为第一个排在该字符前的字符,再再在末尾拼接上左花括号,能够获得前缀的前驱 (predecessor) ,经过给前缀的末尾拼接上左花括号,能够获得前缀的后继 (successor) 。并发
a~z
范围,那么要处理好如下三个问题: P113
UTF-8
、 UTF-16
或者 UTF-32
字符编码(注意: UTF-16
和 UTF-32
只有大端版本可用于上述方法)`
和左花括号 {
步骤: P114
分布式
UUID
)经过向有序集合添加元素来建立查找范围,并在取得范围内的元素以后移除以前添加的元素,这种技术还能够应用在任何已排序索引 (sorted index) 上,而且能经过改善(第七章介绍)应用于几种不一样类型的范围查询,且不须要经过添加元素来建立范围。 P115
ide
P115
分布式锁在业务中也很是常见,可以避免在分布式环境中同时对同一个数据进行操做,进而能够避免并发问题。oop
P119
// 在 conn 上获取 key 的锁,锁超时时间为 expiryTime 毫秒,等待时间最长为 timeout 毫秒
func acquireLock(conn redis.Conn, key string, expiryTime int, timeout int) (token *int) {
// 为了简化,用 纳秒时间戳 当 token ,实际应该用 UUID
value := int(time.Now().UnixNano())
for ; timeout >= 0; {
// 尝试加锁
_, err := redis.String(conn.Do("SET", key, value, "PX", expiryTime, "NX"))
// 若是获取锁成功,则直接返回 token 指针
if err == nil {
return &value
}
// 睡 1ms
time.Sleep(time.Millisecond)
timeout --
}
// timeout 内仍未成功获取锁,则获取失败,返回 nil
return nil
}
// 在 conn 上释放 key 的锁,且锁与 token 对应
func releaseLock(conn redis.Conn, key string, token int) error {
// 用 lua 脚本保证原子性,只有 token 和值相等是才释放
releaseLua := "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
script := redis.NewScript(1, releaseLua)
result, err := redis.Int(script.Do(conn, key, token))
if err != nil {
return err
}
if result == 0 {
return errors.New("release failure")
}
return nil
}
复制代码
P126
计数信号量是一种锁,它可让用户限制一项资源最多能同时被多少个进程访问,一般用于限定可以同时使用的资源数量。 P126
P126
将多个信号量的持有者的信息存储到同一个有序集合中,即为每一个尝试获取的请求生成一个 UUID
,并将这个 UUID
做为有序集合的成员,而成员对应的分值则是尝试获取时的时间戳。 P127
获取信号量步骤: P127
UUID
(时间戳 <= 当时时间戳 - 过时时间)UUID
,使用当时时间戳做为分值,将 UUID
添加到有序集合里面UUID
的排名
UUID
移除释放信号量时直接从有序集合中删除 UUID
便可。若返回值为 1 ,则代表成功手动释放;若返回值为 0 ,则代表已经因为过时而自动释放。 P128
缺点:
UUID
A
的系统时间比 B
的系统时间快 10ms ,那么当 A
取得了最后一个信号量的时候, B
只要在 10ms 内尝试获取信号量,那么就会形成 B
获取了不存在的信号量,致使获取的信号量超过了信号量的总数。 P128
P128
为了实现公平的计数信号量,即先发出获取请求的客户端可以获取到信号量。咱们须要在 Redis
中维护一个自增的计数器,每次发出获取请求前先对其自增,并使用自增后的值做为分值将对应的 UUID
插入到另外一个有序集合中。即本来的有序集合仅用来查找并删除过时的 UUID
,新的有序集合用来获取排名判断请求是否成功获取到信号量。同时为了保持新的有序集合及时删过时的 UUID
,在本来的有序集合执行完删除操做后,还要使用 ZINTERSTORE
命令,保留仅在本来有序集合中出现的 UUID
(ZINTERSTORE count_set 2 count_set time_set WEIGHTS 1 0
)。注意: 若信号量获取失败,则须要及时删除本次插入的无用数据。
上述方法能在必定程度上解决信号量获取数超过信号量总数的问题,但删除过时 UUID
的地方仍是依赖本地时间,因此尽可能保证各个主机的系统时间差距要足够小。 P131
去除本来的有序集合,仅留下计数器和计数值做为分值的有序集合,并对于每一个 UUID
都设置一个有过时时间的键,每次移除前,遍历有序集合,并查询其是否过时,并从有序集合中删除全部已过时的 UUID
。
这样作不只能彻底达到与系统时间无关,还不会存在信号量获取数超过信号量总数的问题,且可以实现单个获取的信号量能有不一样的过时时间,也必定程度上下降了时间复杂度,不过会增长客户端与 Redis
服务器之间的交互次数。
P131
信号量使用者可能在过时时间内没法处理完请求,此时就须要续约,延长过时时间。因为公平的计数信号量已将时间有序集合和计数有序集合分开,因此只须要在时间有序集合中对 UUID
执行 ZADD
便可,若执行失败,则已过时自动释放。 P131
对于我刚刚提出的那种方法,有两种方法能够续约:
lua
脚本保证原子性XX
选项的 SET
命令设置新的过时时间(须要加上原有的过时时间),返回成功则续约成功,不然续约失败P132
两个进程 A
和 B
都在尝试获取剩余的一个信号量时,即便 A
首先对计数器执行了自增操做,但只要 B
可以抢先将本身的 UUID
添加到计数有序集合中,并检查 UUID
的排名,那么 B
就能够成功获取信号量。以后 A
再将本身的 UUID
添加到有序集合里,并检查 UUID
排名,那么 A
也能够成功获取信号量,最终致使获取的信号量多余信号量总数。 P132
为了消除获取信号量时全部可能出现的竞争条件,构建一个正确的计数信号量,咱们须要用到前面完成的带有超时功能的分布式锁。在想要获取信号量时,首先尝试获取分布式锁,若获取锁成功,则继续执行获取信号量的操做;若获取锁失败,那么获取信号量也失败。 P132
P133
本文首发于公众号:满赋诸机(点击查看原文) 开源在 GitHub :reading-notes/redis-in-action