Redis 实战 —— 05. Redis 其他命令简介

2021-01-24  本文已影响0人  满赋诸机

发布与订阅 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

排序 P54

SORT 命令可以对列表、集合和有序集合进行排序 ,可以将 SORT 命令看作使 SQL 中的 order by 子句。 P55

命令 格式 描述
SORT SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] 根据给定的选项,返回或保存给定列表、集合、有序集合 key 中经过排序的元素

可实现功能: P55

相关演示代码如下:

// 执行 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 个命令可以让用户在不被打断的情况下对多个键执行操作,它们分别是: WATCHMULTIEXECUNWATCHDISCART 。基本的 Redis 事务只用 MULTIEXEC 即可,使用多个命令的事务将在以后进行介绍。 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 节。

练习题:提高性能 P58

简单实践 - 文章投票ListArticles 函数在获取整个页面的文章时,需要在 Redis 与客户端之间最多会进行 26 次通信往返,这种做法十分低效,你能否想个办法将 ListArticles 函数的往返次数降低为 2 次呢?

提示:使用流水线

过期时间 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 函数,让它们使用过期时间操作来删除会话信息,从而代替目前使用有序集合来记录并清除会话信息的做法呢?

上一篇下一篇

猜你喜欢

热点阅读