公布与订阅 P52
Redis 实现了公布与订阅(publish/subscribe)模式,又称 pub/sub 模式(与设计模式中的观察者模式相似)。订阅者负责订阅频道,发送者负责向频道发送二进制字符串音讯。每当有音讯被发送至给定频道时,频道的所有订阅者都会接管到音讯。
公布与订阅命令 P52
命令 | 格局 | 形容 |
---|---|---|
SUBSCRIBE | SUBSCRIBE channel [channel …] | 订阅一个或多个频道 |
UNSUBSCRIBE | UNSUBSCRIBE [channel [channel …]] | 退订一个或多个频道;没有指定频道,则退订全副频道 |
PUBLISH | PUBLISH channel message | 给指定频道发送音讯,返回接管到音讯的订阅者数量 |
PSUBSCRIBE | PSUBSCRIBE pattern [pattern …] | 订阅一个或多个模式,与模式匹配的频道均会订阅 |
PUNSUBSCRIBE | PUNSUBSCRIBE [pattern [pattern …]] | 退订一个或多个模式;没有指定模式,则退订全副模式 |
相干演示代码如下:
// 执行公布订阅相干操作(留神:pubSubConn 中的 Conn 对象不能是 conn 对象,即必须建设两个不同的连贯)func executePubSubOperation(pubSubConn redis.PubSubConn, conn redis.Conn) {
// 监听频道音讯并输入
go func() {
for ; ; {switch result := pubSubConn.Receive().(type) {
case redis.Message:
// byte 转 string
resultMap := map[string]string {
"Channel": result.Channel,
"Pattern": result.Pattern,
"Data": string(result.Data),
}
handleResult(resultMap, nil)
case redis.Subscription:
handleResult(result, nil)
}
}
}()
// 订阅两个频道(因为 Subscribe 内没有执行 Receive,所以只有 error,没有谬误时就输入 nil)// 订阅者收到相应的音讯订阅信息,别离输入 -> {subscribe channel_1 1} 和 {subscribe channel_2 2}
handleResult(nil, pubSubConn.Subscribe("channel_1", "channel_2"))
// 订阅两个模式,别离以 _1 和 g_2 为结尾的频道(因为 PSubscribe 内没有执行 Receive,所以只有 error,没有谬误时就输入 nil)// 订阅者收到相应的音讯订阅信息,别离输入 -> {psubscribe *_1 3} 和 {psubscribe *g_2 4}
handleResult(nil, pubSubConn.PSubscribe("*_1", "*g_2"))
time.Sleep(time.Second)
// 公布音讯到频道 channel_1,输入 -> 2,两个订阅者接管到音讯
// 订阅者别离输入 -> map[Channel:channel_1 Data:channel1 Pattern:] 和 map[Channel:channel_1 Data:channel1 Pattern:*_1]
handleResult(conn.Do("PUBLISH", "channel_1", "channel1"))
// 公布音讯到频道 channel_2,输入 -> 1,一个订阅者接管到音讯
// 订阅者输入 -> map[Channel:channel_2 Data:channel1 Pattern:]
handleResult(conn.Do("PUBLISH", "channel_2", "channel1"))
// 退订两个频道(因为 Subscribe 内没有执行 Receive,所以只有 error,没有谬误时就输入 nil)// 订阅者收到相应的音讯退订信息,别离输入 -> {unsubscribe channel_1 3} 和 {unsubscribe channel_2 2}
handleResult(nil, pubSubConn.Unsubscribe("channel_1", "channel_2"))
// 退订两个频道(因为 Subscribe 内没有执行 Receive,所以只有 error,没有谬误时就输入 nil)// 订阅者收到相应的音讯退订信息,别离输入 -> {punsubscribe *_1 1} 和 {punsubscribe *g_2 0}
handleResult(nil, pubSubConn.PUnsubscribe("*_1", "*g_2"))
time.Sleep(time.Second)
}
危险 P54
- 稳定性 :旧版 Redis 的客户端读取音讯不够快时,一直积压的音讯就会使 Redis 的缓冲区越来越大,可能导致 Redis 的速度变慢,甚至间接解体,也有使 Redis 可能被操作系统强制杀死。新版 Redis 会主动断开不合乎
client-output-buffer-limit pubsub
配置选项要求的客户端。 - 可靠性 :任何网络系统在执行操作时都有可能会遇上断线状况,而断线产生的连贯谬误通常会使得网络连接两端中的其中一端进行从新连贯。如果客户端在执行订阅操作的过程中断线,那么客户端将失落在断线期间发送的所有音讯。
排序 P54
SORT
命令能够对列表、汇合和有序汇合进行排序,能够将 SORT
命令看作使 SQL 中的 order by
子句。P55
命令 | 格局 | 形容 |
---|---|---|
SORT | SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern …]] [ASC or DESC] [ALPHA] [STORE destination] | 依据给定的选项,返回或保留给定列表、汇合、有序汇合 key 中通过排序的元素 |
可实现性能:P55
- 依据升序或降序排序元素(应用 [ASC|DESC],默认为升序)
- 将元素看作数字或者字符串进行排序(应用 [ALPHA] 能够当作字符串排序,默认为数字)
- 应用被排序元素之外的其余值作为权重来排序,甚至还能够从输出的列表、汇合、有序汇合以外的其余中央进行取值(应用 [BY pattern] 能够依据指定值排序;能够应用不存在的键作为参数选项跳过排序没间接返回后果)
- 应用被排序元素之外的其余值作为返回后果(应用 [GET pattern [GET pattern …]] 能够依据排序后果返回相应的值)
- 保留排序后果(应用 [STORE destination] 能够指定将后果保留到指定 key,此时返回保留的元素的数量)
- 限度返回后果(应用 [LIMIT offset count] 能够指定要跳过的元素数量和返回的元素数量)
相干演示代码如下:
// 执行 SORT 命令
func executeSortOperation(conn redis.Conn) {
// 删除原有值
handleResult(redis.Int(conn.Do("DEL", "id", "age", "name", "destination")))
// 初始化
handleResult(redis.Int(conn.Do("RPUSH", "id", 1, 4, 3, 2, 5)))
handleResult(redis.String(conn.Do("SET", "age_1", 15)))
handleResult(redis.String(conn.Do("SET", "age_2", 14)))
handleResult(redis.String(conn.Do("SET", "age_3", 11)))
handleResult(redis.String(conn.Do("SET", "age_4", 12)))
handleResult(redis.String(conn.Do("SET", "age_5", 10)))
handleResult(redis.String(conn.Do("SET", "name_1", "tom")))
handleResult(redis.String(conn.Do("SET", "name_2", "jerry")))
handleResult(redis.String(conn.Do("SET", "name_3", "bob")))
handleResult(redis.String(conn.Do("SET", "name_4", "mary")))
handleResult(redis.String(conn.Do("SET", "name_5", "jack")))
// 依据 id 降序排序,跳过第一个元素,获取接下来的两个元素,输入 -> [4 3]
handleResult(redis.Ints(conn.Do("SORT", "id", "LIMIT", "1", "2", "DESC")))
// 依据 age_{id} 升序排序,依照 id age_{id} name_{id} 程序返回后果,输入 -> [5 10 jack 3 11 bob 4 12 mary 2 14 jerry 1 15 tom]
handleResult(redis.Strings(conn.Do("SORT", "id", "BY", "age_*", "GET", "#", "GET", "age_*", "GET", "name_*", "ALPHA")))
// 依据 name_{id} 字典序降序排序,依照 id age_{id} name_{id} 程序返回后果,存储到 destination 中
// 输入 -> 15
handleResult(redis.Int(conn.Do("SORT", "id", "BY", "name_*", "GET", "#", "GET", "age_*", "GET", "name_*", "ALPHA", "DESC", "STORE", "destination")))
// 输入 列表 后果,输入 -> [1 15 tom 4 12 mary 2 14 jerry 5 10 jack 3 11 bob]
handleResult(redis.Strings(conn.Do("LRANGE", "destination", 0, -1)))
}
根本的 Redis 事务
Redis 有 5 个命令能够让用户在不被打断的状况下对多个键执行操作,它们别离是:WATCH
、MULTI
、EXEC
、UNWATCH
和 DISCART
。根本的 Redis 事务只用 MULTI
和 EXEC
即可,应用多个命令的事务将在当前进行介绍。P56
Redis 的根本事务能够让一个客户端在不被其余客户端打断的状况下执行多个命令。当一个事务执行结束之后,Redis 才会解决其余客户端的命令。P56
如果某个 (或某些) key 正处于 WATCH
命令的监督之下,且事务块中有和这个 (或这些) key 相干的命令,那么 EXEC
命令只在这个 (或这些) key 没有被其余命令所改变的状况下执行并失效,否则该事务被打断 (abort)。
命令 | 格局 | 形容 |
---|---|---|
MULTI | MULTI | 标记一个事务块的开始,总是返回 OK |
EXEC | EXEC | 执行所有事务块内的命令,按程序返回命令的执行后果。当操作被打断时,返回 nil |
相干演示代码如下:
// 执行事务命令
func executeTransactionOperation(conn redis.Conn) {
// 删除原有值
handleResult(redis.Int(conn.Do("DEL", "counter")))
// 开启事务(采纳流水线形式,升高通信开销)handleResult(nil, conn.Send("MULTI"))
// 事务中执行自增操作(采纳流水线形式,升高通信开销)handleResult(nil, conn.Send("INCR", "counter"))
handleResult(nil, conn.Send("INCR", "counter"))
handleResult(nil, conn.Send("INCR", "counter"))
// 执行命令,顺次执行自增操作,别离返回操作后果,输入 -> [1 2 3]
handleResult(redis.Ints(conn.Do("EXEC")))
}
练习题:移除竞争条件 P58
简略实际 – 文章投票 中 VoteArticle
函数内曾阐明没有事务管制,会存在并发问题。该函数蕴含一个竞争条件以及一个因为竞争条件而呈现的 bug。函数的竞争条件可能会造成内存透露,而函数的 bug 则可能会导致不正确的投票后果呈现。你能想方法修复它们吗?
提醒:如果你感觉很难了解竞争条件为什么会导致内存透露,那么能够在剖析 简略实际 – 文章投票 中的 PostArticle
的函数的同时,浏览一下 6.2.5 节。
-
感觉还是无奈了解为什么会有这种状况,强行猜想以下可能性(尽管都不是竞争条件造成的):
PostArticle
函数中,在将作者退出到投票用户汇合中后,给其设定过期工夫。如果设定过期工夫之前因为某些原有异样导致没有进行相干操作,那么这个汇合将始终在内存中,不会过期,从而造成内存透露。VoteArticle
函数中,如果将投票用户增加到投票用户汇合中后,还没来得及给文章的相干信息进行设置,那么这个用户当前不能再投票,并且文章的投票信息不对。
-
不是太明确到底在竞争什么,只能针对以上问题解决。用事务只能再增加一个汇合在事务中标记事务是否执行胜利,解决流程大抵如下:
- 先将用户与文章作为值退出到这个汇合
- 再将用户退出到投票汇合中
- 而后开启事务,顺次发送更新信息的命令和删除那个汇合中的相干信息,并执行
- 最初有一个 worker 扫描这个汇合,将其中值拿进去解析出用户和文章,再查改用户是否已在汇合中,如果在汇合中,则从新执行 步骤 3,最初删除该值
练习题:进步性能 P58
简略实际 – 文章投票 中 ListArticles
函数在获取整个页面的文章时,须要在 Redis 与客户端之间最多会进行 26 次通信往返,这种做法非常低效,你是否想个办法将 ListArticles
函数的往返次数升高为 2 次呢?
提醒:应用流水线
- 获取文章列表时,先获取相应的 id 列表(最多 25 个),再循环获取每个 id 对应的文章,所以最多会进行 26 次通信往返
-
因为必须先获取 id 列表,再获取每个 id 对应的文章,所以只能将这两块离开,所以最低至多有 2 次通信往返。大抵流程如下:
- 先获取 id 列表
- 应用流水线,顺次将获取每个 id 的文章的命令发送至缓冲区,最初与服务端通信并执行命令(Go 中能够应用上述事务演示代码的形式进行操作)
- 最初依照程序解析后果
过期工夫 P58
只有少数几个命令能够原子地为键设置过期工夫,并且对于列表、汇合、哈希表和有序汇合这样的容器来说,键过期命令只能为整个键设置过期工夫,而没方法为键外面的单个元素设置过期工夫(能够应用存储工夫戳的有序汇合来实现针对单个元素的过期工夫;也能够以前缀的模式将容器中的单个元素变为字符串)。P58
用于解决过期工夫的 Redis 命令 P59
命令 | 格局 | 形容 |
---|---|---|
PERSIST | PERSIST key | 移除键的过期工夫 |
TTL | TTL key | 查看键间隔过期工夫还有多少秒 |
EXPIRE | EXPIRE key seconds | 让键在指定的秒数之后过期 |
EXPIREAT | EXPIREAT key timestamp | 让键在指定的 UNIX 秒级工夫戳过期 |
PTTL | PTTL key | 查看键间隔过期工夫还有多少毫秒 |
PEXPIRE | PEXPIRE key milliseconds | 让键在指定的毫秒数之后过期 |
PEXPIREAT | PEXPIREAT key milliseconds-timestamp | 让键在指定的 UNIX 毫秒级工夫戳过期 |
相干演示代码如下:
// 指定过期工夫相干的命令
func executeExpirationOperation(conn redis.Conn) {
// 删除原有值
handleResult(redis.Int(conn.Do("DEL", "string")))
// 设置字符串的值为 value,输入 -> OK,string 变为 -> value
handleResult(redis.String(conn.Do("SET", "string", "value")))
// 查看 string 的过期工夫,输入 -> -1,示意不过期
handleResult(redis.Int(conn.Do("TTL", "string")))
// 设置 string 在 3 秒后过期,输入 -> 1
handleResult(redis.Int(conn.Do("EXPIRE", "string", 3)))
time.Sleep(time.Second)
// 查看 string 的过期工夫,输入 -> 2
handleResult(redis.Int(conn.Do("TTL", "string")))
// 移除 string 的过期工夫,输入 -> 1
handleResult(redis.Int(conn.Do("PERSIST", "string")))
// 查看 string 的过期工夫,输入 -> -1,示意不过期
handleResult(redis.Int(conn.Do("TTL", "string")))
// 设置 string 在以后工夫 2500 毫秒后过期,输入 -> 1
handleResult(redis.Int(conn.Do("PEXPIREAT", "string", time.Now().UnixNano() / 1e6 + 2500)))
time.Sleep(time.Second)
// 查看 string 的过期工夫,输入 -> 1499,示意还有 1499 毫秒过期
handleResult(redis.Int(conn.Do("PTTL", "string")))
time.Sleep(2 * time.Second)
// 查看 string 的过期工夫,输入 -> -2,示意已过期
handleResult(redis.Int(conn.Do("PTTL", "string")))
}
练习题:应用 EXPIRE 命令代替工夫戳有序汇合 P59
在简略实际 – Web 利用中应用了一个依据工夫戳排序、用于革除会话信息的有序汇合,通过这个有序汇合,程序能够在清理会话的时候,对用户浏览过的商品以及用户购物车外面的商品进行剖析。然而,如果咱们决定不对商品进行剖析的话,那么就能够应用 Redis 提供的过期工夫操作来主动清理过期的会话信息,而无须应用清理函数。那么,你是否批改简略实际 – Web 利用中定义的 UpdateToken
函数和 UpdateCartItem
函数,让它们应用过期工夫操作来删除会话信息,从而代替目前应用有序汇合来记录并革除会话信息的做法呢?
UpdateToken
函数:令牌于userId
的对应关系不在存储于哈希表中,而是以前缀的模式将容器中的单个元素变为字符串(下面提到过),并设置过期工夫,并移除最近操作工夫有序汇合,这样令牌到期后就会主动删除,不须要清理函数了。UpdateCartItem
函数:因为过后此处把 Redis 当作数据库应用,认为购物车不应该随登录态的生效而隐没,所以购物车与userId
挂钩,不存在上述问题。然而如果要让购物车也主动过期,就须要在UpdateToken
函数内同时设置购物车的过期工夫即可。
本文首发于公众号:满赋诸机(点击查看原文)开源在 GitHub:reading-notes/redis-in-action