Redis必知必会

什么是Redis

Redis是一个开源、基于内存的键值对存储数据库。通常情况下,键的类型都是字符串,值的类型有多种,Redis支持值的数据结构如下:

  1. 字符串
  2. 散列
  3. 列表
  4. 集合
  5. 有序集合
  6. 位图

Redis键和常见的值类型

键类型(key) 特点 备注
二进制安全 任何二进制序列作为键
较短的键占用内存较少
非常短的键,不利于语义化 需要在键的可读性和更少的内存消耗,找到合适平衡点。
尝试坚持使用固定的模式 例如,object-type:id => user:1000
键最大为512MB
Strings SET/GET 设置和获取字符串值 与Redis键关联最简单的值类型
支持包括二进制数据的字符串,最大512MB
SET nx 如果key不存在就设置值
SET xx 如果key存在才更新值
SET ex 设置指定的过期时间,多少秒
SET px 设置指定过期时间,多少毫秒
INCR key 解析字符串作为整数,自增1,得到的值作为新的值
EXISTS key 检查当前数据库是否存在给定的键
DEL key 删除键和关联的值
TTL key 检查键的剩余生存时间
列表(List) 通过链表实现 在常量时间内添加新元素到头部或者尾部,访问元素所需时间跟元素的索引成正比
LPUSH key value 将新元素添加到列表左侧,也就是头部
RPUSH key value 将新元素添加到列表右侧,也就是尾部
LRANGE key 0 size-1 从列表获取指定范围的元素,可以为负数。-1是最后一个元素,-2列表倒数第2个元素
RPOP key 从列表查找元素并同时从列表尾部弹出元素
LPOP key 从列表查找元素并同时从列表头部弹出元素
LTRIM key 0 2 只保存最新的N个元素,并丢弃所有最旧的元素
BRPOP key 阻塞版本的RPOP,等待指定列表的元素到达,若超时就立即返回。
BLPOP key 阻塞版本的LPOP,等待指定列表的元素到达,若超时就立即返回。
客户端以有序方式获取元素,返回值为2个元素的数组,超时则返回NULL
hash(哈希) hmset key k1 v1 k2 v2 设置哈希键多个字段值
hget key k1 获取散列中的指定字段
hget key k2
hmget key k1 k2 返回散列表中的多个字段值
hincrby key k1 value 对散列表中的指定字段自增值
集合(Set) 该集合是无序的
sadd myset 1 2 3 给指定集合添加多个元素
smembers myset 返回指定集合的元素
sismember myset 3 检查集合中元素是否存在
sinter k1 k2 k3 执行多个键间的交集
spop key 随机删除一个集合中的元素
sunionstore k1 k2 获取多个集合对应的并集,存储到第一个集合中。
scard key 获取指定集合的元素个数
有序集合(Sort Set) zadd key score value 添加有序集合的分数和值
内部实现是采用跳表+散列表实现。
zrang key 0 -1 返回指定有序集合全部元素,默认为升序
zrevrangekey 0 -1 倒序返回元素
zrangebyscore key -inf 1950 获取指定的排序之后的元素
zremrangebyscore key 1940 1960 删除指定范围内的元素
zrank key value 查询值所在的位置
位图(bit) SETBIT key 10 1 设置指定位图值
GETBIT key 10 获取指定位图值
BITCOUNT key 计算设置为1位的计数

为什么要用Redis

  1. Redis的访问速度快,完全基于内存操作,网络层采用epoll单线程解决高并发的问题。
  2. Redis有着丰富的数据类型,常见有5种:string、Hash、List、Set、SortSet、Bit等。

怎么用Redis

Redis列表

列表对很多任务都有用,比如:

  1. 记住用户发布到社交网络的最新更新。
  2. 流程之间的通信。
  3. 使用生产者将消息推入列表的消费者-生产者模式。

Redis集合

标记新闻文章的标签。假设文章id为1000,有标签1,2,5,77,那么你可以设计:

sadd news:1000:tags 1 2 55 77

也许你想要反向关系,标签对应的文章列表

sadd tag:1:news 1000
sadd tag:2:news 1000
sadd tag:5:news 1000
sadd tag:77:news 1000

smembers news:1000:tags

为了模拟基于网络的扑克游戏,你可以使用(C)lubs(黑梅花),(D)iamonds(红方块),(H)earts(红心),(S)pades(黑桃) 标识牌面的不同花色:

sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK
   D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3
   H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6
   S7 S8 S9 S10 SJ SQ SK

Redis位图

实现一个用户每日访问的最长连续性。位索引为,获取当前的unix时间戳,减去初始偏移量,然后除以3600*24。对于每一个用户都有一个包含每天访问信息的小字符串。使用BITCOUNT获取给定用户访问网站的天数,BITPOS调用,计算出最长的天数。

假设网站公开的日期为20190502,转化为时间戳为1556726400,那么位索引的计算公式为=(当前00:00:00的时间戳-1556726400)/(3600*24)取整,那么20190502的索引为0,20190503的索引,那么刚好=1。通过BITCOUNT可以计算出用户在指定期间访问网站的天数。

参考链接

  1. 介绍Redis数据类型和抽象
  2. 数据类型
  3. 为什么要用Redis

练习 Web 爬虫

问题

在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。

修改 Crawl 函数来并行地抓取 URL,并且保证不重复。

提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!

背景知识

  1. 学习 Go语言的sync.Mutex用法,阅读计数器实现的源代码,这个例子也可以参数计数器来实现。
  2. 在多个协程抓取时可以添加互斥锁,这也是Mutex的应用。
  3. 需要你深刻的理解协程的编程方式,也需要注意信道的数据获取方式。

实现思路

  1. 基本代码算是实现了,但是没有做协程安全,如果多个协程同时处理,就会有变量被覆盖的情况。
  2. 还有一种情况,就是并没有对爬虫进行去重判断,有可能实现的爬虫会一直死循环,并需要对map进行互斥锁判断,避免会有冲突。
  3. URL的抓取实现是一些测试用例,并不是真实世界的抓取,现实中的爬虫会比这个更复杂,简单的来讲,最少要添加一个超时的处理,异常的处理。
  4. 在Crawl中如何实现协程抓取URL?如何同步控制map的URL更新?
  5. 定义SafeCounter类型,类似计数器有两个变量,一个是字符串和整数的映射,另外一个是互斥锁的实现。定义一个全局变量sc,在全部的函数里都可以使用。
  6. 由于map不是协程安全,所有验证函数validUrl和标记函数markUrl函数,都需要加下互斥锁。
  7. 并行的抓取,可以在Crawl实现,对抓取回来的内容里的二级URL进行协程处理,并发抓取URL。
    难点在于下面这两个地方:
//方案1
//开启协程处理二级URL
ch := make(chan int, len(urls))
for _, u := range urls {
    go crawl(u, depth-1, fetcher, ch)
}
for i:=0; i < len(urls); i++ {
    //从信道接受数据
     <- ch
}

func crawl(url string, depth int, fetcher Fetcher, ch chan int) {
    Crawl(url, depth, fetcher)
    ch <- 1
}

第一个是,根据对应urls数量开启配置信道缓冲区大小。
通过for循环urls数量,实现并行协程处理,然后重点来了,从信道缓冲区,接受协程发送过来的数据。
第二个,在私有函数crawl,注意,调用完抓取函数Crawl后,需要关闭信道。

细节

一级URL 标题 内容里的二级URL
https://golang.org/ “The Go Programming Language” "https://golang.org/pkg/","https://golang.org/cmd/"
https://golang.org/pkg/ “Packages” "https://golang.org/","https://golang.org/cmd/","https://golang.org/pkg/fmt/","https://golang.org/pkg/os/",
https://golang.org/pkg/fmt/ “Package fmt” "https://golang.org/","https://golang.org/pkg/",
https://golang.org/pkg/os/ “Package os” "https://golang.org/","https://golang.org/pkg/",

实现代码

GitHub

参考链接

  1. 练习:Web 爬虫
  2. GO指南 练习:Web爬虫
  3. 学习一下golang 练习70 web crawler

练习 等价二叉查找树

问题

不同二叉树的叶节点上可以保存相同的值序列。例如,以下两个二叉树都保存了序列 `1,1,2,3,5,8,13`。


在大多数语言中,检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。

本例使用了 tree 包,它定义了类型:

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}
1. 实现 Walk 函数。

2. 测试 Walk 函数。

函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, ..., 10k。

创建一个新的信道 ch 并且对其进行步进:

go Walk(tree.New(1), ch)
然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, ..., 10。

3. 用 Walk 实现 Same 函数来检测 t1 和 t2 是否存储了相同的值。

4. 测试 Same 函数。

Same(tree.New(1), tree.New(1)) 应当返回 true,而 Same(tree.New(1), tree.New(2)) 应当返回 false。

Tree 的文档可在[这里](https://godoc.org/golang.org/x/tour/tree#Tree)找到。

背景知识

  1. 你需要了解什么是等价二叉树?
  2. 阅读Tree类型的Go实现源代码。代码实现了3个函数,New函数新建二叉树,insert函数插入新元素,实现了打印字符串String接口
  3. 了解协程,信道的实现原理。
  4. 判断等价二叉树相等的话,那么左右节点的值都需要相等。

实现思路

  1. 实现 Walk 函数,循环通过信道发送数据,然后接受并输出。Walk的遍历方式为中序遍历,先遍历根节点,然后左节点,最后右节点。
  2. 注意tree的遍历方式,也就是获得值的方式,实现一个私有的walk函数,签名和Walk一致,因为Tree类型较为简单,并不能直接判断循环的结束。故采用这样的方式,循环遍历到最后,t == nil时,关闭信道,不然会报错的。
  3. 实现 Same 函数,循环判断tree每一个值是否相等。难点在于,创建2个信道,去读取Walk返回的Tree的节点值。

实现代码

GitHub

参考链接

  1. 练习:等价二叉查找树
  2. 二叉查找树
  3. Go指南练习之《等价二叉树》(Equivalent Binary Trees)