使用Go与redis构建有趣的应用
2020-03-01 304浏览
- 1.使⽤用 Go 和 Redis 构建有趣的程序 ⻩黄健宏 @ huangz.me
- 2.关于我 • ⻩黄健宏,⽹网名 huangz ,⼴广东清远⼈人。 • 计算机技术图书作者和译者,偶尔也写⼀一点⼩小程序⾃自娱⾃自乐。 • 精通 Go、 Python 、 Ruby 、 PHP、 C 等数⼗十种语⾔言……的 Hello World ! • 著作:《Redis 设计与实现》,《Redis 使⽤用教程》(写作中)。 • 翻译:《Go Web 编程》,《Redis 实战》。 • 开源⽂文档:《Go 标准库中⽂文⽂文档》,《Redis 命令参考》,《SICP 解题集》等。 • 个⼈人⽹网站: huangz.me 。
- 3.路路线图
- 4.路路线图 ⼀一. Redis 简介
- 5.路路线图 ⼀一. Redis 简介 ⼆二. 使⽤用 Redis 构建锁
- 6.路路线图 ⼀一. Redis 简介 ⼆二. 使⽤用 Redis 构建锁 三. 使⽤用 Redis 构建在线⽤用户统计器器
- 7.路路线图 ⼀一. Redis 简介 ⼆二. 使⽤用 Redis 构建锁 三. 使⽤用 Redis 构建在线⽤用户统计器器 四. 使⽤用 Redis 构建⾃自动补完程序
- 8.Redis an open source, in-memory data structure store
- 9.特点
- 10.特点 • 具有多种不不同的数据结构可⽤用,其中包括:字符串串、散列列、列列表、集合、有序集合、位图 (bitmap)、HyperLogLog、地理理坐标(GEO)
- 11.特点 • 具有多种不不同的数据结构可⽤用,其中包括:字符串串、散列列、列列表、集合、有序集合、位图 (bitmap)、HyperLogLog、地理理坐标(GEO) • 内存存储和基于多路路复⽤用的事件响应系统,确保了了命令请求的执⾏行行速度和效率
- 12.特点 • 具有多种不不同的数据结构可⽤用,其中包括:字符串串、散列列、列列表、集合、有序集合、位图 (bitmap)、HyperLogLog、地理理坐标(GEO) • 内存存储和基于多路路复⽤用的事件响应系统,确保了了命令请求的执⾏行行速度和效率 • 丰富的附加功能:事务、Lua 脚本、键过期机制、键淘汰机制、多种持久化⽅方式(AOF、RDB、 RDB+AOF 混合)
- 13.特点 • 具有多种不不同的数据结构可⽤用,其中包括:字符串串、散列列、列列表、集合、有序集合、位图 (bitmap)、HyperLogLog、地理理坐标(GEO) • 内存存储和基于多路路复⽤用的事件响应系统,确保了了命令请求的执⾏行行速度和效率 • 丰富的附加功能:事务、Lua 脚本、键过期机制、键淘汰机制、多种持久化⽅方式(AOF、RDB、 RDB+AOF 混合) • 强⼤大的多机功能⽀支持:主从复制(单主多从)、Sentinel(⾼高可⽤用)、集群(基于 Raft 算法,多 主多从,内建⾼高可⽤用)
- 14.特点 • 具有多种不不同的数据结构可⽤用,其中包括:字符串串、散列列、列列表、集合、有序集合、位图 (bitmap)、HyperLogLog、地理理坐标(GEO) • 内存存储和基于多路路复⽤用的事件响应系统,确保了了命令请求的执⾏行行速度和效率 • 丰富的附加功能:事务、Lua 脚本、键过期机制、键淘汰机制、多种持久化⽅方式(AOF、RDB、 RDB+AOF 混合) • 强⼤大的多机功能⽀支持:主从复制(单主多从)、Sentinel(⾼高可⽤用)、集群(基于 Raft 算法,多 主多从,内建⾼高可⽤用) • 拥有⽆无限可能性的扩展模块系统:神经⽹网络、全⽂文搜索、JSON 数据结构等等。
- 15.数据结构 data structures
- 16.字符串串 索引 0 1 2 3 4 5 6 7 8 9 字符 ‘h’ ‘e’ ‘l’ ‘l’ ‘o’ ‘ ’ ‘w’ ‘o’ ‘r’ ‘l’ 10 11 ‘d’ ‘\0’ 带索引、带⻓长度记录、⼆二进制安全、带有内存预分配策略略的字符串串实现
- 17.散列列 键 值 “name” “peter” “age” 32 “gender” “male” 使⽤用哈希表储存键值对、每个键都各不不相同、获取单个键值对的复杂度为常数
- 18.列列表 索引 0 项 “job::6379” 1 2 3 “msg::10086” “request::256” “user::peter” 使⽤用双端链表实现的有序序列列、针对两端的操作为常数复杂度、其他列列表操作多为线性复杂度、允许重复元素
- 19.集合 “tom” “peter” “jack” “david” “mary” 以⽆无序⽅方式储存任意多个各不不相同的元素、针对单个元素的操作都为常数复杂度、可执⾏行行并集交集等常⻅见的集合计算
- 20.有序集合 分值 成员 1.5 ”banana” 2.5 “cherry” 3.7 “apple” 8.3 “durian” 各不不相同的多个成员按分值⼤大⼩小进⾏行行排列列、可以按分值顺序或者成员顺序执⾏行行多项有序操作、使⽤用 Skip List 实现
- 21.位图(bitmap) 索引 0 1 2 3 4 5 6 7 位 0 1 1 0 1 1 1 0 由⼀一连串串⼆二进制位组成、可单独设置指定的位、可获取指定范围的多个位⼜又或者对它们进⾏行行计数
- 22.HyperLogLog “peter” HyperLogLog 算法 0100101010 1001010101 0010101000 1010100100 1010101001 0101010010 32 基于概率算法实现、可以计算出给定集合的近似基数、只使⽤用固定⼤大⼩小的内存
- 23.地理理位置(GEO) 113.2099647, 23.593675 “Qingyuan“ 113.106308, 23.0088312 “Guangzhou” 储存经纬度坐标、可以进⾏行行范围计算、或者计算两地间的距离
- 24.键 “msg” “profile” “job-queue” “team-member” “fruit-price” 值 “hello world” “name” “age” “gender” “peter” 32 “male” “job::6379” “msg::10086” “request::256” “user::peter” “peter”, “jack”, “tom”, “mary”, “david” 1.5 ”banana” 2.5 “cherry” 3.7 “apple” 8.3 “durian”
- 25.命令请求 COMMAND… OPTION1… 命令 键 参数 选项 选项的值
- 26.命令请求 COMMAND… OPTION1… 命令 键 参数 选项 选项的值 PING SET msg “hello world” SORT fruit ALPHA GET *-price HMSET profile “name” “peter” “age” 32 “gender” “male” RPUSH “job-queue” “job::6379” “msg::10086” “request::256” “user::peter” …
- 27.获取客户端
- 28.获取客户端 go get github.com/mediocregopher/radix.v2
- 29.连接客户端 package main import ( "fmt" "github.com/mediocregopher/radix.v2/redis" ) func main() { client, _ := redis.Dial("tcp", "localhost:6379") defer client.Close() repl := client.Cmd("PING") content, _ := repl.Str() fmt.Println(content) // "PONG" }
- 30.连接客户端 package main import ( "fmt" "github.com/mediocregopher/radix.v2/redis" ) func main() { client, _ := redis.Dial("tcp", "localhost:6379") defer client.Close() repl := client.Cmd("PING") content, _ := repl.Str() fmt.Println(content) // "PONG" } 连接服务器器
- 31.连接客户端 package main import ( "fmt" "github.com/mediocregopher/radix.v2/redis" ) func main() { client, _ := redis.Dial("tcp", "localhost:6379") defer client.Close() repl := client.Cmd("PING") content, _ := repl.Str() 执⾏行行命令并获取回复 fmt.Println(content) // "PONG" } 连接服务器器
- 32.连接客户端 package main import ( "fmt" "github.com/mediocregopher/radix.v2/redis" ) func main() { client, _ := redis.Dial("tcp", "localhost:6379") defer client.Close() 将回复转换为字符串串 repl := client.Cmd("PING") content, _ := repl.Str() 执⾏行行命令并获取回复 fmt.Println(content) // "PONG" } 连接服务器器
- 33.锁 lock
- 34.锁 锁是⼀一种同步机制, 它可以保证⼀一项资源在任何时候只能被⼀一个进程使⽤用, 如果有其他进程想要 使⽤用相同的资源, 那么它们就必须等待, 直到正在使⽤用资源的进程放弃使⽤用权为⽌止。
- 35.锁 锁是⼀一种同步机制, 它可以保证⼀一项资源在任何时候只能被⼀一个进程使⽤用, 如果有其他进程想要 使⽤用相同的资源, 那么它们就必须等待, 直到正在使⽤用资源的进程放弃使⽤用权为⽌止。 ⼀一个锁实现通常会有获取(acquire)和释放(release)这两种操作:
- 36.锁 锁是⼀一种同步机制, 它可以保证⼀一项资源在任何时候只能被⼀一个进程使⽤用, 如果有其他进程想要 使⽤用相同的资源, 那么它们就必须等待, 直到正在使⽤用资源的进程放弃使⽤用权为⽌止。 ⼀一个锁实现通常会有获取(acquire)和释放(release)这两种操作: • 获取操作⽤用于取得资源的独占使⽤用权。 在任何时候, 最多只能有⼀一个进程取得锁, 我们把成功 取得锁的进程称之为锁的持有者。 在锁已经被持有的情况下, 所有尝试再次获取锁的操作都会 失败。
- 37.锁 锁是⼀一种同步机制, 它可以保证⼀一项资源在任何时候只能被⼀一个进程使⽤用, 如果有其他进程想要 使⽤用相同的资源, 那么它们就必须等待, 直到正在使⽤用资源的进程放弃使⽤用权为⽌止。 ⼀一个锁实现通常会有获取(acquire)和释放(release)这两种操作: • 获取操作⽤用于取得资源的独占使⽤用权。 在任何时候, 最多只能有⼀一个进程取得锁, 我们把成功 取得锁的进程称之为锁的持有者。 在锁已经被持有的情况下, 所有尝试再次获取锁的操作都会 失败。 • 释放操作⽤用于放弃资源的独占使⽤用权, ⼀一般由锁的持有者调⽤用。 在锁被释放之后, 其他进程就 可以尝试再次获取这个锁了了。
- 38.⽅方法⼀一 —— 使⽤用字符串串 将⼀一个字符串串键⽤用作锁,如果这个键有值,那么说明锁已被获取;反之,如果键没有值,那么说明 锁未被获取,程序可以通过为其设置值来获取锁。
- 39.⽅方法⼀一 —— 使⽤用字符串串 将⼀一个字符串串键⽤用作锁,如果这个键有值,那么说明锁已被获取;反之,如果键没有值,那么说明 锁未被获取,程序可以通过为其设置值来获取锁。 未 加 锁 键 值 “LOCK_KEY” nil
- 40.⽅方法⼀一 —— 使⽤用字符串串 将⼀一个字符串串键⽤用作锁,如果这个键有值,那么说明锁已被获取;反之,如果键没有值,那么说明 锁未被获取,程序可以通过为其设置值来获取锁。 未 加 锁 已 加 锁 键 值 键 值 “LOCK_KEY” nil “LOCK_KEY” “LOCKING”
- 41.需要⽤用到的命令
- 42.需要⽤用到的命令 GET key 获取字符串串键 key 的值,如果该键尚未有值,那么返回⼀一个空值(nil)
- 43.需要⽤用到的命令 GET key 获取字符串串键 key 的值,如果该键尚未有值,那么返回⼀一个空值(nil) SET key value 将字符串串键 key 的值设置为 value ,如果键已经有值,那么默认使⽤用新值去覆盖旧值
- 44.需要⽤用到的命令 GET key 获取字符串串键 key 的值,如果该键尚未有值,那么返回⼀一个空值(nil) SET key value 将字符串串键 key 的值设置为 value ,如果键已经有值,那么默认使⽤用新值去覆盖旧值 DEL key 删除给定的键 key
- 45.实现代码 const lock_key = "LOCK_KEY" const lock_value = "LOCK_VALUE" func acquire(client *redis.Client) bool { current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("SET", lock_key, lock_value) return true } else { return false } } func release(client *redis.Client) { client.Cmd("DEL", lock_key) }
- 46.实现代码 const lock_key = "LOCK_KEY" const lock_value = "LOCK_VALUE" func acquire(client *redis.Client) bool { current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("SET", lock_key, lock_value) return true } else { return false } } func release(client *redis.Client) { client.Cmd("DEL", lock_key) } 获取键的值
- 47.实现代码 const lock_key = "LOCK_KEY" const lock_value = "LOCK_VALUE" func acquire(client *redis.Client) bool { current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("SET", lock_key, lock_value) 为键设置值 return true } else { return false } } func release(client *redis.Client) { client.Cmd("DEL", lock_key) } 获取键的值
- 48.实现代码 const lock_key = "LOCK_KEY" const lock_value = "LOCK_VALUE" func acquire(client *redis.Client) bool { current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("SET", lock_key, lock_value) 为键设置值 return true } else { return false } } func release(client *redis.Client) { client.Cmd("DEL", lock_key) } 删除键 获取键的值
- 49.竞争条件 const lock_key = "LOCK_KEY" const lock_value = "LOCK_VALUE" func acquire(client *redis.Client) bool { current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("SET", lock_key, lock_value) return true } else { 因为 GET 和 SET 在两个不不同的请求中执⾏行行,在它们之间可 return false 能有其他请求已经改变了了锁键的值,导致“锁键为空”这⼀一判 } 断不不再为真,从⽽而引发多个客户端之间的竞争条件。 } func release(client *redis.Client) { client.Cmd("DEL", lock_key) }
- 50.客户端⼀一 客户端⼆二 开始 开始
- 51.客户端⼀一 客户端⼆二 开始 开始 GET LOCK_KEY GET LOCK_KEY
- 52.客户端⼀一 客户端⼆二 开始 开始 GET LOCK_KEY GET LOCK_KEY SET LOCK_KEY
- 53.客户端⼀一 客户端⼆二 开始 开始 GET LOCK_KEY GET LOCK_KEY SET LOCK_KEY 成功获取锁
- 54.客户端⼀一 客户端⼆二 开始 开始 GET LOCK_KEY GET LOCK_KEY SET LOCK_KEY 成功获取锁
- 55.客户端⼀一 客户端⼆二 开始 开始 GET LOCK_KEY GET LOCK_KEY SET LOCK_KEY 不不知道锁键已被 修改,继续执⾏行行 SET 命令。 SET LOCK_KEY 成功获取锁
- 56.客户端⼀一 客户端⼆二 开始 开始 GET LOCK_KEY GET LOCK_KEY SET LOCK_KEY 不不知道锁键已被 修改,继续执⾏行行 SET 命令。 同时存在两个锁,出错! SET LOCK_KEY 成功获取锁 成功获取锁
- 57.⽅方法⼆二 —— 使⽤用事务保证安全性 锁的基本实现⽅方法跟之前⼀一样,但使⽤用 Redis 的事务特性保证操作的安全性。
- 58.⽅方法⼆二 —— 使⽤用事务保证安全性 锁的基本实现⽅方法跟之前⼀一样,但使⽤用 Redis 的事务特性保证操作的安全性。 ⾮非 事 务 命 命 命 命 命 令 令 令 令 令 命 令 执⾏行行器器
- 59.⽅方法⼆二 —— 使⽤用事务保证安全性 锁的基本实现⽅方法跟之前⼀一样,但使⽤用 Redis 的事务特性保证操作的安全性。 ⾮非 事 务 命 命 命 命 命 令 令 令 令 令 命 令 执⾏行行器器 事 务 命 命 命 命 事 命 令 令 令 务 令 命令A 命令B 命令C 令 执⾏行行器器
- 60.需要⽤用到的命令
- 61.需要⽤用到的命令 WATCH key [key …] 监视给定的键,如果这些键在事务执⾏行行之前已经被修改,那么拒绝执⾏行行事务
- 62.需要⽤用到的命令 WATCH key [key …] 监视给定的键,如果这些键在事务执⾏行行之前已经被修改,那么拒绝执⾏行行事务 MULTI 开启⼀一个事务,之后客户端发送的所有操作命令都会被放⼊入到事务队列列⾥里里⾯面
- 63.需要⽤用到的命令 WATCH key [key …] 监视给定的键,如果这些键在事务执⾏行行之前已经被修改,那么拒绝执⾏行行事务 MULTI 开启⼀一个事务,之后客户端发送的所有操作命令都会被放⼊入到事务队列列⾥里里⾯面 EXEC 尝试执⾏行行事务,成功时将返回⼀一个由多个命令回复组成的队列列,失败则返回 nil
- 64.实现代码 func acquire(client *redis.Client) bool { client.Cmd("WATCH", lock_key) current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("MULTI") client.Cmd("SET", lock_key, lock_value) repl, _ := client.Cmd("EXEC").List() if repl != nil { return true } } return false }
- 65.实现代码 func acquire(client *redis.Client) bool { client.Cmd("WATCH", lock_key) 监视键 current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("MULTI") client.Cmd("SET", lock_key, lock_value) repl, _ := client.Cmd("EXEC").List() if repl != nil { return true } } return false }
- 66.实现代码 func acquire(client *redis.Client) bool { client.Cmd("WATCH", lock_key) 监视键 current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("MULTI") 开启事务 client.Cmd("SET", lock_key, lock_value) repl, _ := client.Cmd("EXEC").List() if repl != nil { return true } } return false }
- 67.实现代码 func acquire(client *redis.Client) bool { client.Cmd("WATCH", lock_key) 监视键 current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("MULTI") 放⼊入事务队列列 开启事务 client.Cmd("SET", lock_key, lock_value) repl, _ := client.Cmd("EXEC").List() if repl != nil { return true } } return false }
- 68.实现代码 func acquire(client *redis.Client) bool { client.Cmd("WATCH", lock_key) 监视键 current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("MULTI") 放⼊入事务队列列 开启事务 client.Cmd("SET", lock_key, lock_value) repl, _ := client.Cmd("EXEC").List() 尝试执⾏行行事务 if repl != nil { return true } } return false }
- 69.实现代码 func acquire(client *redis.Client) bool { client.Cmd("WATCH", lock_key) 监视键 current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("MULTI") 放⼊入事务队列列 开启事务 client.Cmd("SET", lock_key, lock_value) repl, _ := client.Cmd("EXEC").List() 尝试执⾏行行事务 if repl != nil { 根据事务是否执⾏行行成功 return true 来判断加锁是否成功 } } return false }
- 70.客户端⼀一 客户端⼆二 开始 开始
- 71.客户端⼀一 客户端⼆二 开始 开始 WATCH LOCK_KEY WATCH LOCK_KEY GET LOCK_KEY GET LOCK_KEY
- 72.客户端⼀一 客户端⼆二 开始 开始 WATCH LOCK_KEY WATCH LOCK_KEY GET LOCK_KEY GET LOCK_KEY MULTI SET LOCK_KEY EXEC 事务执⾏行行成功
- 73.客户端⼀一 客户端⼆二 开始 开始 WATCH LOCK_KEY WATCH LOCK_KEY GET LOCK_KEY GET LOCK_KEY MULTI SET LOCK_KEY EXEC 成功获取锁 事务执⾏行行成功
- 74.客户端⼀一 客户端⼆二 开始 开始 WATCH LOCK_KEY WATCH LOCK_KEY GET LOCK_KEY GET LOCK_KEY MULTI MULTI SET LOCK_KEY SET LOCK_KEY EXEC EXEC 成功获取锁 事务执⾏行行成功
- 75.客户端⼀一 客户端⼆二 开始 开始 WATCH LOCK_KEY WATCH LOCK_KEY GET LOCK_KEY GET LOCK_KEY MULTI 因为 WATCH 命 令的作⽤用,该事 务将被拒绝执⾏行行。 MULTI SET LOCK_KEY SET LOCK_KEY EXEC EXEC 成功获取锁 事务执⾏行行成功
- 76.客户端⼀一 客户端⼆二 开始 开始 WATCH LOCK_KEY WATCH LOCK_KEY GET LOCK_KEY GET LOCK_KEY MULTI 因为 WATCH 命 令的作⽤用,该事 务将被拒绝执⾏行行。 MULTI SET LOCK_KEY SET LOCK_KEY EXEC EXEC 成功获取锁 获取锁失败 事务执⾏行行成功
- 77.使⽤用事务带来的代价 func acquire(client *redis.Client) bool { client.Cmd("WATCH", lock_key) current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("MULTI") client.Cmd("SET", lock_key, lock_value) repl, _ := client.Cmd("EXEC").List() if repl != nil { return true } 事务的使⽤用导致程序的逻辑变得复 } 杂起来,并且不不正确地使⽤用事务本 return false 身就有引发 bug 的危险。 }
- 78.带 NX 选项的 SET 命令 SET key value NX 只在键 key 不不存在的情况下,将它的值设置为 value ,然后返回 OK; 如果键已经有值,那么放弃执⾏行行设置操作,并返回 nil 表示设置失败。 NX 代表 Not eXists , 该选项从 Redis 2.6.12 版本开始可⽤用。
- 79.NX 选项的作⽤用 func acquire(client *redis.Client) bool { current_value, _ := client.Cmd("GET", lock_key).Str() if current_value == "" { client.Cmd("SET", lock_key, lock_value) return true } else { return false } } 相当于在服务器器 ⾥里里⾯面以原⼦子⽅方式 执⾏行行这两项操作
- 80.实现三 —— 使⽤用带 NX 选项的 SET 命令 func acquire(client *redis.Client) bool { repl, _ := client.Cmd("SET", lock_key, lock_value, "NX").Str() return repl != "" } 嘿,我在这⼉儿!
- 81.客户端⼀一 客户端⼆二 开始 开始
- 82.客户端⼀一 客户端⼆二 开始 开始 SET LOCK_KEY NX 键尚未有值,设置成功
- 83.客户端⼀一 客户端⼆二 开始 开始 SET LOCK_KEY NX 成功获取锁 键尚未有值,设置成功
- 84.客户端⼀一 客户端⼆二 开始 开始 SET LOCK_KEY NX 键已经有值,设置失败 SET LOCK_KEY NX 成功获取锁 键尚未有值,设置成功
- 85.客户端⼀一 客户端⼆二 开始 开始 SET LOCK_KEY NX 键已经有值,设置失败 SET LOCK_KEY NX 获取锁失败 成功获取锁 键尚未有值,设置成功
- 86.在线⽤用户统计器器 online user counter
- 87.示例例 analytics.google.com panda.tv bilibili.com v2ex.com
- 88.⽅方法⼀一 —— 使⽤用集合 当⼀一个⽤用户上线时,将它的⽤用户名添加到记录在线⽤用户的集合当中。
- 89.⽅方法⼀一 —— 使⽤用集合 当⼀一个⽤用户上线时,将它的⽤用户名添加到记录在线⽤用户的集合当中。 j a c k 未 上 “tom” “peter” 线
- 90.⽅方法⼀一 —— 使⽤用集合 当⼀一个⽤用户上线时,将它的⽤用户名添加到记录在线⽤用户的集合当中。 j a c k 未 上 线 j a c 已 k “tom” “peter” 上 “tom” “peter” “jack” 线
- 91.需要⽤用到的命令
- 92.需要⽤用到的命令 SADD set element [element …] 将给定的元素添加到集合当中
- 93.需要⽤用到的命令 SADD set element [element …] 将给定的元素添加到集合当中 SCARD set 获取集合的基数,也即是集合包含的元素数量量
- 94.需要⽤用到的命令 SADD set element [element …] 将给定的元素添加到集合当中 SCARD set 获取集合的基数,也即是集合包含的元素数量量 SISMEMBER set element 检查给定的元素是否存在于集合当中,是的话返回 1 ,不不是的话返回 0
- 95.实现代码 const online_user_set = "ONLINE_USER_SET" func set_online(client *redis.Client, user string) { client.Cmd("SADD", online_user_set, user) } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("SCARD", online_user_set).Int64() return repl } func is_online_or_not(client *redis.Client, user string) bool { repl, _ := client.Cmd("SISMEMBER", online_user_set, user).Int() return repl == 1 }
- 96.实现代码 const online_user_set = "ONLINE_USER_SET" func set_online(client *redis.Client, user string) { client.Cmd("SADD", online_user_set, user) 把⽤用户名添加⾄至集合 } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("SCARD", online_user_set).Int64() return repl } func is_online_or_not(client *redis.Client, user string) bool { repl, _ := client.Cmd("SISMEMBER", online_user_set, user).Int() return repl == 1 }
- 97.实现代码 const online_user_set = "ONLINE_USER_SET" func set_online(client *redis.Client, user string) { client.Cmd("SADD", online_user_set, user) 把⽤用户名添加⾄至集合 } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("SCARD", online_user_set).Int64() return repl } 获取集合基数 func is_online_or_not(client *redis.Client, user string) bool { repl, _ := client.Cmd("SISMEMBER", online_user_set, user).Int() return repl == 1 }
- 98.实现代码 const online_user_set = "ONLINE_USER_SET" func set_online(client *redis.Client, user string) { client.Cmd("SADD", online_user_set, user) 把⽤用户名添加⾄至集合 } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("SCARD", online_user_set).Int64() return repl } 获取集合基数 func is_online_or_not(client *redis.Client, user string) bool { repl, _ := client.Cmd("SISMEMBER", online_user_set, user).Int() return repl == 1 } 检查⽤用户是否存在 于集合当中
- 99.问题
- 100.问题 集合的体积将随着元素的增加⽽而增加,集合包含的元素越多,每个元素的体积越⼤大,集合的体积也 就越⼤大。
- 101.问题 集合的体积将随着元素的增加⽽而增加,集合包含的元素越多,每个元素的体积越⼤大,集合的体积也 就越⼤大。 假设平均每个⽤用户的名字⻓长度为 10 字节,那么: • • 拥有⼀一百万⽤用户的⽹网站每天需要使⽤用 10 MB 内存去储存在线⽤用户统计信息 拥有⼀一千万⽤用户的⽹网站每天需要使⽤用 100 MB 内存去储存在线⽤用户统计信息
- 102.问题 集合的体积将随着元素的增加⽽而增加,集合包含的元素越多,每个元素的体积越⼤大,集合的体积也 就越⼤大。 假设平均每个⽤用户的名字⻓长度为 10 字节,那么: • • 拥有⼀一百万⽤用户的⽹网站每天需要使⽤用 10 MB 内存去储存在线⽤用户统计信息 拥有⼀一千万⽤用户的⽹网站每天需要使⽤用 100 MB 内存去储存在线⽤用户统计信息 如果我们把这些信息储存⼀一年年,那么: • • 拥有⼀一百万⽤用户的⽹网站每年年需要为此使⽤用 3.65 GB 内存 拥有⼀一千万⽤用户的⽹网站每年年需要为此使⽤用 36.5 GB 内存
- 103.问题 集合的体积将随着元素的增加⽽而增加,集合包含的元素越多,每个元素的体积越⼤大,集合的体积也 就越⼤大。 假设平均每个⽤用户的名字⻓长度为 10 字节,那么: • • 拥有⼀一百万⽤用户的⽹网站每天需要使⽤用 10 MB 内存去储存在线⽤用户统计信息 拥有⼀一千万⽤用户的⽹网站每天需要使⽤用 100 MB 内存去储存在线⽤用户统计信息 如果我们把这些信息储存⼀一年年,那么: • • 拥有⼀一百万⽤用户的⽹网站每年年需要为此使⽤用 3.65 GB 内存 拥有⼀一千万⽤用户的⽹网站每年年需要为此使⽤用 36.5 GB 内存 除此之外,因为使⽤用 Redis 储存信息还有⼀一些额外的消耗(overhead),所以实际的内存占⽤用数量量 将⽐比这个估算值更更⾼高。
- 104.⽅方法⼆二 —— 使⽤用位图 为每个⽤用户创建⼀一个相对应的数字 ID ,当⼀一个⽤用户上线时,使⽤用他的 ID 作为索引,将位图指定索 引上的⼆二进制位设置为 1 。
- 105.⽅方法⼆二 —— 使⽤用位图 为每个⽤用户创建⼀一个相对应的数字 ID ,当⼀一个⽤用户上线时,使⽤用他的 ID 作为索引,将位图指定索 引上的⼆二进制位设置为 1 。 ⽤用户名 “peter”
- 106.⽅方法⼆二 —— 使⽤用位图 为每个⽤用户创建⼀一个相对应的数字 ID ,当⼀一个⽤用户上线时,使⽤用他的 ID 作为索引,将位图指定索 引上的⼆二进制位设置为 1 。 ⽤用户名 “peter” ID 10086
- 107.⽅方法⼆二 —— 使⽤用位图 为每个⽤用户创建⼀一个相对应的数字 ID ,当⼀一个⽤用户上线时,使⽤用他的 ID 作为索引,将位图指定索 引上的⼆二进制位设置为 1 。 ⽤用户名 “peter” ID 10086 索引 0 1 位 0 1 10085 10086 10087 … 0 1 1 … N-1 N 1 0
- 108.需要⽤用到的命令
- 109.需要⽤用到的命令 SETBIT bitmap index value 将位图指定索引上的⼆二进制位设置为给定的值
- 110.需要⽤用到的命令 SETBIT bitmap index value 将位图指定索引上的⼆二进制位设置为给定的值 GETBIT bitmap index 获取位图指定索引上的⼆二进制位
- 111.需要⽤用到的命令 SETBIT bitmap index value 将位图指定索引上的⼆二进制位设置为给定的值 GETBIT bitmap index 获取位图指定索引上的⼆二进制位 BITCOUNT bitmap 统计位图中值为 1 的⼆二进制位的数量量
- 112.实现代码 const online_user_bitmap = "ONLINE_USER_BITMAP" func set_online(client *redis.Client, user_id int64) { client.Cmd("SETBIT", online_user_bitmap, user_id, 1) } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("BITCOUNT", online_user_bitmap).Int64() return repl } func is_online_or_not(client *redis.Client, user_id int64) bool { repl, _ := client.Cmd("GETBIT", online_user_bitmap, user_id).Int() return repl == 1 }
- 113.实现代码 const online_user_bitmap = "ONLINE_USER_BITMAP" func set_online(client *redis.Client, user_id int64) { client.Cmd("SETBIT", online_user_bitmap, user_id, 1) } 设置⼆二进制位 func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("BITCOUNT", online_user_bitmap).Int64() return repl } func is_online_or_not(client *redis.Client, user_id int64) bool { repl, _ := client.Cmd("GETBIT", online_user_bitmap, user_id).Int() return repl == 1 }
- 114.实现代码 const online_user_bitmap = "ONLINE_USER_BITMAP" func set_online(client *redis.Client, user_id int64) { client.Cmd("SETBIT", online_user_bitmap, user_id, 1) } 设置⼆二进制位 func count_online(client *redis.Client) int64 { 统计值为 1 的⼆二 repl, _ := client.Cmd("BITCOUNT", online_user_bitmap).Int64() 进制位数量量 return repl } func is_online_or_not(client *redis.Client, user_id int64) bool { repl, _ := client.Cmd("GETBIT", online_user_bitmap, user_id).Int() return repl == 1 }
- 115.实现代码 const online_user_bitmap = "ONLINE_USER_BITMAP" func set_online(client *redis.Client, user_id int64) { client.Cmd("SETBIT", online_user_bitmap, user_id, 1) } 设置⼆二进制位 func count_online(client *redis.Client) int64 { 统计值为 1 的⼆二 repl, _ := client.Cmd("BITCOUNT", online_user_bitmap).Int64() 进制位数量量 return repl } func is_online_or_not(client *redis.Client, user_id int64) bool { repl, _ := client.Cmd("GETBIT", online_user_bitmap, user_id).Int() return repl == 1 检查指定⼆二进制位的值 }
- 116.改进 虽然位图的体积仍然会随着⽤用户数量量的增多⽽而变⼤大,但因为记录每个⽤用户所需的内存数量量从原来的 平均 10 字节变成了了 1 位,所以实现⽅方法⼆二将节约⼤大量量内存。
- 117.改进 虽然位图的体积仍然会随着⽤用户数量量的增多⽽而变⼤大,但因为记录每个⽤用户所需的内存数量量从原来的 平均 10 字节变成了了 1 位,所以实现⽅方法⼆二将节约⼤大量量内存。 储存⼀一天⽤用户在线信息所需的内存数量量: • • ⼀一百万⼈人,125 KB ; ⼀一千万⼈人, 1250 KB = 1.25 MB ;
- 118.改进 虽然位图的体积仍然会随着⽤用户数量量的增多⽽而变⼤大,但因为记录每个⽤用户所需的内存数量量从原来的 平均 10 字节变成了了 1 位,所以实现⽅方法⼆二将节约⼤大量量内存。 储存⼀一天⽤用户在线信息所需的内存数量量: • • ⼀一百万⼈人,125 KB ; ⼀一千万⼈人, 1250 KB = 1.25 MB ; 储存⼀一年年⽤用户在线信息所需的内存数量量: • • ⼀一百万⼈人,45.625 MB ⼀一千万⼈人,456.25 MB
- 119.⽅方法三 —— 使⽤用 HyperLogLog 当⼀一个⽤用户上线时,使⽤用统计在线⽤用户数量量的 HyperLogLog 对其进⾏行行计数。
- 120.⽅方法三 —— 使⽤用 HyperLogLog 当⼀一个⽤用户上线时,使⽤用统计在线⽤用户数量量的 HyperLogLog 对其进⾏行行计数。 “jack”
- 121.⽅方法三 —— 使⽤用 HyperLogLog 当⼀一个⽤用户上线时,使⽤用统计在线⽤用户数量量的 HyperLogLog 对其进⾏行行计数。 “jack” HyperLogLog 算法
- 122.⽅方法三 —— 使⽤用 HyperLogLog 当⼀一个⽤用户上线时,使⽤用统计在线⽤用户数量量的 HyperLogLog 对其进⾏行行计数。 “jack” HyperLogLog 算法 0100101010 1001010101 0010101000 1010100100 1010101001 0101010010
- 123.⽅方法三 —— 使⽤用 HyperLogLog 当⼀一个⽤用户上线时,使⽤用统计在线⽤用户数量量的 HyperLogLog 对其进⾏行行计数。 “jack” HyperLogLog 算法 0100101010 1001010101 0010101000 1010100100 1010101001 0101010010 32+1
- 124.需要⽤用到的命令
- 125.需要⽤用到的命令 PFADD hll element [element …] 使⽤用 HyperLogLog 对给定元素进⾏行行计数
- 126.需要⽤用到的命令 PFADD hll element [element …] 使⽤用 HyperLogLog 对给定元素进⾏行行计数 PFCOUNT hll 获取 HyperLogLog 的近似基数,也即是基数的估算值
- 127.需要⽤用到的命令 PFADD hll element [element …] 使⽤用 HyperLogLog 对给定元素进⾏行行计数 PFCOUNT hll 获取 HyperLogLog 的近似基数,也即是基数的估算值 缺陷:因为 HyperLogLog 的概率性质,我们⽆无法准确地知道特定的⼀一个⽤用户是否 在线,换句句话说,这个实现只能给出在线⽤用户的数量量,⽆无法检测⽤用户是否在线。
- 128.实现代码 const online_user_hll = "ONLINE_USER_HLL" func set_online(client *redis.Client, user string) { client.Cmd("PFADD", online_user_hll, user) } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("PFCOUNT", online_user_hll).Int64() return repl }
- 129.实现代码 const online_user_hll = "ONLINE_USER_HLL" func set_online(client *redis.Client, user string) { client.Cmd("PFADD", online_user_hll, user) 对⽤用户进⾏行行计数 } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("PFCOUNT", online_user_hll).Int64() return repl }
- 130.实现代码 const online_user_hll = "ONLINE_USER_HLL" func set_online(client *redis.Client, user string) { client.Cmd("PFADD", online_user_hll, user) 对⽤用户进⾏行行计数 } func count_online(client *redis.Client) int64 { repl, _ := client.Cmd("PFCOUNT", online_user_hll).Int64() return repl } 获取近似基数
- 131.三种实现的内存消耗对⽐比 规模/实现 ⼀一百万,⼀一天 ⼀一千万,⼀一天 ⼀一百万,⼀一年年 ⼀一千万,⼀一年年 集合 位图 HyperLogLog
- 132.三种实现的内存消耗对⽐比 规模/实现 集合 ⼀一百万,⼀一天 10 MB ⼀一千万,⼀一天 100 MB ⼀一百万,⼀一年年 3.65 GB ⼀一千万,⼀一年年 36.5 GB 位图 HyperLogLog
- 133.三种实现的内存消耗对⽐比 规模/实现 集合 位图 ⼀一百万,⼀一天 10 MB 125 KB ⼀一千万,⼀一天 100 MB 1.25 MB ⼀一百万,⼀一年年 3.65 GB 45.625 MB ⼀一千万,⼀一年年 36.5 GB 456.25 MB HyperLogLog
- 134.三种实现的内存消耗对⽐比 规模/实现 集合 位图 HyperLogLog ⼀一百万,⼀一天 10 MB 125 KB 12 KB ⼀一千万,⼀一天 100 MB 1.25 MB 12 KB ⼀一百万,⼀一年年 3.65 GB 45.625 MB 4.32 MB ⼀一千万,⼀一年年 36.5 GB 456.25 MB 4.32 MB
- 135.⾃自动补全 autocomplete
- 136.示例例
- 137.示例例
- 138.示例例
- 139.示例例
- 140.原理理解释
- 141.原理理解释 > go
- 142.原理理解释 > go “google” “gmail” “gopher” “great” “guide”
- 143.原理理解释 > go “google” “gmail” “gopher” “great” “guide” 输⼊入与候选结果
- 144.原理理解释 > go “google” “gmail” “gopher” “great” “guide” 输⼊入与候选结果 w1, w2, w3, w4, w5, “google” “gmail” “gopher” “great” “guide” 权重与候选结果
- 145.原理理解释 > go “google” “gmail” “gopher” “great” “guide” 输⼊入与候选结果 w1, w2, w3, w4, w5, “google” “gmail” “gopher” “great” “guide” 权重与候选结果 分值 成员 w1 “google” w2 “gmail” w3 “gopher” w4 “great” w5 “guide” 使⽤用有序集合储存权重表
- 146.构建权重表
- 147.构建权重表 分值 成员 320 “google” 300 “gmail” 278 “great” 277 “gopher” 210 “guide” 现有的权重表
- 148.构建权重表 分值 成员 320 “google” 300 “gmail” 278 “great” 277 “gopher” 210 “guide” 现有的权重表 > gopher > gopher > gopher 获得三个新的输⼊入
- 149.构建权重表 分值 成员 分值 成员 320 “google” 320 “google” 300 “gmail” 300 “gmail” 278 “great” 280 “gopher” 277 “gopher” 278 “great” 210 “guide” 210 “guide” 现有的权重表 > gopher > gopher > gopher 获得三个新的输⼊入 根据输⼊入更更新权重
- 150.构建多个权重表
- 151.构建多个权重表 > gopher 根据输⼊入
- 152.构建多个权重表 > gopher 根据输⼊入 ”g“ ”go“ ”gop“ ”goph“ ”gophe“ ”gopher“ 枚举出字符串串的组成排列列
- 153.构建多个权重表 > gopher 根据输⼊入 ”g“ ”go“ ”gop“ ”goph“ ”gophe“ ”gopher“ 枚举出字符串串的组成排列列 分值 成员 320 “google” 300 “gmail” 280 +1 “gopher” 278 “great” 210 “guide” 根据排列列对各个权重表进⾏行行更更新
- 154.补全过程
- 155.补全过程 > g
- 156.补全过程 分值 成员 320 “google” 300 “gmail” 280 “gopher” 278 “great” 210 “guide” > g
- 157.补全过程 分值 成员 320 “google” 300 “gmail” 280 “gopher” 278 “great” 210 “guide” > g > go
- 158.补全过程 分值 成员 分值 成员 320 “google” 320 “google” 300 “gmail” 280 “gopher” 280 “gopher” 255 “goodbye” 278 “great” 230 “gold” 210 “guide” 188 “gossip” > g > go
- 159.补全过程 分值 成员 分值 成员 320 “google” 320 “google” 300 “gmail” 280 “gopher” 280 “gopher” 255 “goodbye” 278 “great” 230 “gold” 210 “guide” 188 “gossip” > g > go > gop
- 160.补全过程 分值 成员 分值 成员 分值 成员 320 “google” 320 “google” 280 “gopher” 300 “gmail” 280 “gopher” 255 “gopak” 280 “gopher” 255 “goodbye” 232 “gopher hold” 278 “great” 230 “gold” 180 “gopura” 210 “guide” 188 “gossip” 75 “gophering“ > g > go > gop
- 161.需要⽤用到的命令
- 162.需要⽤用到的命令 ZINCRBY zset increment member 对给定成员的分值执⾏行行⾃自增操作
- 163.需要⽤用到的命令 ZINCRBY zset increment member 对给定成员的分值执⾏行行⾃自增操作 ZREVRANGE zset start end [WITHSCORES] 按照分值从⼤大到⼩小的顺序,从有序集合⾥里里⾯面获取指定索引范围内的成员
- 164.实现代码 const autocomplete = "autocomplete::" func feed(client *redis.Client, content string, weight int) { for i, _ := range content { segment := content[:i+1] key := autocomplete + segment client.Cmd("ZINCRBY", key, weight, content) } } func hint(client *redis.Client, prefix string, count int) []string { key := autocomplete + prefix result, _ := client.Cmd("ZREVRANGE", key, 0, count-1).List() return result }
- 165.实现代码 const autocomplete = "autocomplete::" func feed(client *redis.Client, content string, weight int) { for i, _ := range content { 枚举字符串串 segment := content[:i+1] key := autocomplete + segment 组成排列列 client.Cmd("ZINCRBY", key, weight, content) } } func hint(client *redis.Client, prefix string, count int) []string { key := autocomplete + prefix result, _ := client.Cmd("ZREVRANGE", key, 0, count-1).List() return result }
- 166.实现代码 const autocomplete = "autocomplete::" func feed(client *redis.Client, content string, weight int) { for i, _ := range content { 拼接出各个权重表的键名 枚举字符串串 segment := content[:i+1] key := autocomplete + segment 组成排列列 client.Cmd("ZINCRBY", key, weight, content) } } func hint(client *redis.Client, prefix string, count int) []string { key := autocomplete + prefix result, _ := client.Cmd("ZREVRANGE", key, 0, count-1).List() return result }
- 167.实现代码 const autocomplete = "autocomplete::" func feed(client *redis.Client, content string, weight int) { for i, _ := range content { 拼接出各个权重表的键名 枚举字符串串 segment := content[:i+1] key := autocomplete + segment 组成排列列 client.Cmd("ZINCRBY", key, weight, content) 对各个权重表进⾏行行更更新 } } func hint(client *redis.Client, prefix string, count int) []string { key := autocomplete + prefix result, _ := client.Cmd("ZREVRANGE", key, 0, count-1).List() return result }
- 168.实现代码 const autocomplete = "autocomplete::" func feed(client *redis.Client, content string, weight int) { for i, _ := range content { 拼接出各个权重表的键名 枚举字符串串 segment := content[:i+1] key := autocomplete + segment 组成排列列 client.Cmd("ZINCRBY", key, weight, content) 对各个权重表进⾏行行更更新 } } func hint(client *redis.Client, prefix string, count int) []string { 拼接出权重 key := autocomplete + prefix 表的键名 result, _ := client.Cmd("ZREVRANGE", key, 0, count-1).List() return result }
- 169.实现代码 const autocomplete = "autocomplete::" func feed(client *redis.Client, content string, weight int) { for i, _ := range content { 拼接出各个权重表的键名 枚举字符串串 segment := content[:i+1] key := autocomplete + segment 组成排列列 client.Cmd("ZINCRBY", key, weight, content) 对各个权重表进⾏行行更更新 } } func hint(client *redis.Client, prefix string, count int) []string { 拼接出权重 key := autocomplete + prefix 表的键名 result, _ := client.Cmd("ZREVRANGE", key, 0, count-1).List() 按权重从⼤大到⼩小 return result 获取候选结果 }
- 170.总结
- 171.总结 • Go 和 Redis 都是简单且强⼤大的⼯工具,组合使⽤用它们能够轻⽽而易易举地解决很多过去⾮非常难以实现 或者需要很多代码才能实现的特性(⼜又⿊黑我⼤大 JAVA ,放学别⾛走!)。
- 172.总结 • Go 和 Redis 都是简单且强⼤大的⼯工具,组合使⽤用它们能够轻⽽而易易举地解决很多过去⾮非常难以实现 或者需要很多代码才能实现的特性(⼜又⿊黑我⼤大 JAVA ,放学别⾛走!)。 • 在构建程序的时候⼀一定要确保程序的正确性和安全性,虽然为了了保证这两点常常会使得程序变得 复杂,但有时候⼯工具本身也会提供⼀一些⻥鱼和熊掌兼得的⽅方案。
- 173.总结 • Go 和 Redis 都是简单且强⼤大的⼯工具,组合使⽤用它们能够轻⽽而易易举地解决很多过去⾮非常难以实现 或者需要很多代码才能实现的特性(⼜又⿊黑我⼤大 JAVA ,放学别⾛走!)。 • 在构建程序的时候⼀一定要确保程序的正确性和安全性,虽然为了了保证这两点常常会使得程序变得 复杂,但有时候⼯工具本身也会提供⼀一些⻥鱼和熊掌兼得的⽅方案。 • 解决⼀一个问题通常会有很多不不同的⽅方式可选,但我们必须对正在使⽤用的⼯工具⾜足够熟悉,才能找到 这些⽅方法。
- 174.总结 • Go 和 Redis 都是简单且强⼤大的⼯工具,组合使⽤用它们能够轻⽽而易易举地解决很多过去⾮非常难以实现 或者需要很多代码才能实现的特性(⼜又⿊黑我⼤大 JAVA ,放学别⾛走!)。 • 在构建程序的时候⼀一定要确保程序的正确性和安全性,虽然为了了保证这两点常常会使得程序变得 复杂,但有时候⼯工具本身也会提供⼀一些⻥鱼和熊掌兼得的⽅方案。 • 解决⼀一个问题通常会有很多不不同的⽅方式可选,但我们必须对正在使⽤用的⼯工具⾜足够熟悉,才能找到 这些⽅方法。 • 最后,不不同实现的效率和功能通常也会有所不不同,我们要根据⾃自身的情况进⾏行行选择,不不要盲⽬目的 相信所谓的“最优解”。
- 175.多谢⼤大家,得闲饮茶! thank you!
- 176.©⻩黄健宏, 2017 · 保留留所有权利利,禁⽌止未经许可的转载和商⽤用