Redis 实战 —— 08. 实现自动补全、分布式锁和计数信号量
自动补全 P109
自动补全在日常业务中随处可见,应该算一种最常见最通用的功能。实际业务场景肯定要包括包含子串的情况,其实这在一定程度上转换成了搜索功能,即包含某个子串的串,且优先展示前缀匹配的串。如果仅包含前缀,那么可以使用 Trie 树,但在包含其他的情况下,使用数据库/ ES 本身自带查询就足够了。可以按照四种情况(精确匹配、前缀、后缀、包含(也可将后两种融合成包含)),分别查询结果,直至达到数据条数上限或者全部查询完毕。但这种使用方法有缺点:查询次数多、难以分页。不过实际场景中需要补全的情况都只要第一页的数据即可。
自动补全最近联系人 P110
需求: 记录最近联系过的 100 个人名,并支持对输入的串进行自动补全。 P110
数据量很小,所以可以在 Redis 中用列表维护最近联系人,然后在内存中进行过滤可自动补全的串。
步骤: P111
- 维护长度为 100 的最近联系人列表
- 如果指定的联系人已在列表中,则从列表中移除 (
LREM) - 将指定的联系人添加到列表最前面 (
LPUSH) - 如果添加完成后,列表长度超过 100 ,则对列表进行修剪,仅保留列表 前面的 100 个联系人 (
LTRIM)
- 如果指定的联系人已在列表中,则从列表中移除 (
- 获取整个最近联系人列表,在内存中根据四种情况进行过滤即可
通讯录自动补全 P112
需求: 有很多通讯录,每个通讯录中有几千个人(仅包含小写英文字母),尽量减少 Redis 传输给客户端的数据量,实现前缀自动补全。 P112
思路: 使用有序集合存储人名,利用有序集合的特性:当成员的分值相同时,将根据成员字符串的二进制顺序进行排序。如果要查找 abc 前缀的字符串,那么实际上就是查找介于 abbz... 之后和 abd 之前的字符串。所以问题转化为:如何找到第一个排在 abc 之前的元素的排名 和 第一个排在 abd 之前的元素的排名。我们可以构造两个不在有序集合中的字符串 (abb{, abc{) 辅助定位,因为 { 是排在 z 后第一个不适用的字符,这样可以保证这两个字符串不存在与有序集合中,且满足转化后的问题的限制。 P113
综上: 通过将给定前缀的最后一个字符替换为第一个排在该字符前的字符,再再在末尾拼接上左花括号,可以得到前缀的前驱 (predecessor) ,通过给前缀的末尾拼接上左花括号,可以得到前缀的后继 (successor) 。
- 字符集:当处理的字符不仅仅限于
a~z范围,那么要处理好以下三个问题:P113- 将所有字符转换为字节:使用
UTF-8、UTF-16或者UTF-32字符编码(注意:UTF-16和UTF-32只有大端版本可用于上述方法) - 找出需要支持的字符范围,确保所选范围的前面和后面至少留有一个字符
- 使用位于范围前后的字符分别代替反引号
`和左花括号{
- 将所有字符转换为字节:使用
步骤: P114
- 运用思路中的方法找到前缀的前驱和后继(为了防止同时查询相同的前缀出现错误,可以在前驱和后继之后添加上
UUID) - 将前驱和后继插入到有序集合里
- 查看前驱和后继的排名
- 取出他们之间的元素
- 从有序集合中删除前驱和后继
通过向有序集合添加元素来创建查找范围,并在取得范围内的元素之后移除之前添加的元素,这种技术还可以应用在任何已排序索引 (sorted index) 上,并且能通过改善(第七章介绍)应用于几种不同类型的范围查询,且不需要通过添加元素来创建范围。 P115
分布式锁 P115
分布式锁在业务中也非常常见,能够避免在分布式环境中同时对同一个数据进行操作,进而可以避免并发问题。
导致锁出现不正确行为,以及锁在不正确运行时的症状 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的排名- 若排名低于可获取的信号量总数(成员排名从 0 开始计算),那么表示成功获取了信号量
- 若排名等于或高于可获取的信号量总数,那么未获取成功,需要将刚刚的
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
Redis 实战 —— 08. 实现自动补全、分布式锁和计数信号量的更多相关文章
- [Git]08 如何自动补全命令
[Git]08如何自动补全命令 如果你用的是 Bash shell,可以试试看 Git 提供的自动完成脚本.下载 Git 的源代码,进入 contrib/completion 目录,会看到一个g ...
- 第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)用Django实现搜索的自动补全功能
第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—用Django实现搜索的自动补全功能 elasticsearch(搜索引擎)提供了自动补全接口 官方说明:https://www.e ...
- 利用redis完成自动补全搜索功能(一)
最近要做一个搜索自动补全的功能(目前只要求做最前匹配),自动补全就是自动提示,类似于搜索引擎,再上面输入一个字符,下面会提示多个关键词供参考,比如你输入 nb 2字符, 会自动提示nba,nba录像, ...
- 使用Redis实现中英文自动补全功能详解
1.Redis自动补全功能介绍: Redis可以帮我们实现很多种功能,今天这里着重介绍的是Redis的自动补全功能的实现.我们使用有序集合,并score都为0,这样就按元素值的字典序排序.然后我们 ...
- Autocomplete 自动补全(Webform实战篇)
开篇语 因为项目中需要用到一个自动补全的功能,功能描述: 需求一:新增收件人的时候,自动下拉显示出数据库中所有的收件人信息(显示的信息包括:姓名-收件地址-联系方式) 需求二:选中一个值得时候,分别赋 ...
- 四十七 Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)用Django实现搜索的自动补全功能
elasticsearch(搜索引擎)提供了自动补全接口 官方说明:https://www.elastic.co/guide/en/elasticsearch/reference/current/se ...
- Linux实战(12):解决Centos7 docker 无法自动补全
环境:centos最小化安装,会出现一些命令无法自动补全的情况,例如在docker start 无法自动补全 start 命令,无法自动补全docker容器名字.出现这种情况的可参考以下操作: yum ...
- 使用DWR实现自动补全 类似百度搜索框的自动显示效果
使用DWR实现自动补全 自动补全:是指用户在文本框中输入前几个字母或汉字的时候,自动在存放数据的文件或数据库中将所有以这些字母或汉字开头的数据提示给用户供用户选择 在日常上网过程中,我们经常使用搜索引 ...
- 关于notepad++如何自动补全标签的问题
转自:https://blog.csdn.net/Panda_Eyes1/article/details/81486331 关于notepad++如何自动补全标签的问题 2018年08月07日 18: ...
随机推荐
- matplotlib学习日记(十一)---坐标轴高阶应用
(一)设置坐标轴的位置和展示形式 (1)向画布中任意位置添加任意数量的坐标轴 ''' 通过在画布的任意位置和区域,讲解设置坐标轴的位置和坐标轴的展示形式的实现方法, 与subplot,subplots ...
- spring boot编程思想(核心篇) pdf 下载 it教程
资料简介:本书是<Spring Boot 编程思想>的核心篇,开篇总览Spring Boot核心特性,接着讨论自动装配(Auto-Configuration)与SpringApplicat ...
- Python文件部分(不包括数据)
一,基本操作过程:1.a = open(文件名 ,打开方式) 2.a.read(size) | a.readline(size) | a.readlines(hint) 或 a.write(s) | ...
- 初识Web Service
Web Service 今天新接了一个小项目,要用webservice.把示例代码拿过来一看,我有点懵.这啥东西?虽然调试了一下,找猫画虎也算调成功了,但是对这个webservice还是不太了解. 下 ...
- 5. Longest Palindromic Substring最大回文子串
int sta = 0; int max = 1; public String longestPalindrome(String s) { /* 判断回文有两种: 1.最大回文子序列求长度: 用动态规 ...
- 高性能MySQL学习总结二----常见数据类型选择及优化
一.数据类型的选择 MySQL的数据类型有很多种,选择正确的数据类型对于获得高性能特别地重要,如何选择合适的数据类型呢?主要遵从以下三个原则: 1.更小的通常情况下性能更好 一般情况下,应该尽量使用可 ...
- Java JVM——8.堆
堆的核心概念 堆针对一个 JVM 进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的. 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域. J ...
- TurtleBot3 Waffle (tx2版华夫)(3)opencr系统安装
3. 1. 安装 Arduino IDE Opencr的安装环境的安装包,双击打开即可.进入安装的过程. 在这里你可以选择自己的安装位置. 安装已经完成,点击CLOSE 即可. 3.2. opencr ...
- 整合.NET WebAPI和 Vuejs——在.NET单体应用中使用 Vuejs 和 ElementUI
.NET简介 .NET 是一种用于构建多种应用的免费开源开发平台,例如: Web 应用.Web API 和微服务 云中的无服务器函数 云原生应用 移动应用 桌面应用 1). Windows WPF 2 ...
- .NET 云原生架构师训练营(模块二 基础巩固 MongoDB API实现)--学习笔记
2.5.7 MongoDB -- API实现 问题查询单个实现 问题查询列表实现 问题跨集合查询实现 问题创建实现 问题更新实现 问题回答实现 问题评论实现 问题投票实现 回答实现 QuestionC ...
