Redis之应用篇

Redis基础入门篇可参考 Redis基础入门篇

一、分布式锁

分布式应用进行逻辑处理时经常会遇到并发问题。比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。)这个时候就要使用到分布式锁来限制程序的并发执行。

分布式锁本质上要实现的目标就是在 Redis 里面占一个“位”,当别的进程也要来占时,发现已经有人坐在那里了,就只好放弃或者稍后再试。

占位一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占位。先来先占, 用完了,再调用 del 指令释放位置。

127.0.0.1:6379> setnx userName alanchen
(integer) 1
127.0.0.1:6379> del userName
(integer) 1

但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。

127.0.0.1:6379> setnx userName alanchen
(integer) 1
127.0.0.1:6379> expire userName 5
(integer) 1
127.0.0.1:6379> del userName
(integer) 1

但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。

这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。也许你会想到用 Redis 事务来解决。但是这里不行,因为 expire 是依赖于 setnx 的执行结果的,如果 setnx 没抢到锁,expire 是不应该执行的。事务里没有 if-else 分支逻辑,事务的特点是一口气执行,要么全部执行要么一个都不执行。

Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,解决了分布式锁的问题。

127.0.0.1:6379> setex userName 5 alanchen
OK
127.0.0.1:6379> get userName
(nil)

setex等价于 set+expire。或者

127.0.0.1:6379> set userName alanchen ex 5 nx
OK
127.0.0.1:6379> get userName
(nil)
  • nx表示只有当userName对应的key值不存在的时候才能set成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。
  • ex 5表示这个锁有一个5秒的自动过期时间。

超时问题
Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,这个时候第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。

为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。

二、消息队列

我们平时习惯于使用 Rabbitmq 和 Kafka 作为消息队列中间件,来给应用程序之间增加异步消息传递功能。这两个中间件都是专业的消息队列中间件,特性之多超出了大多数人的理解能力。

使用过 Rabbitmq 的同学知道它使用起来有多复杂,发消息之前要创建 Exchange,再创建 Queue,还要将 Queue 和 Exchange 通过某种规则绑定起来,发消息的时候要指定 routing-key,还要控制头部信息。消费者在消费消息之前也要进行上面一系列的繁琐过程。但是绝大多数情况下,虽然我们的消息队列只有一组消费者,但还是需要经历上面这些繁琐的过程。

有了 Redis,它就可以让我们解脱出来,对于那些只有一组消费者的消息队列,使用 Redis 就可以非常轻松的搞定。Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。

2.1 异步消息队列

Redis 的 list(列表) 数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,使用lpop 和 rpop来出队列。

2.2 队列空了怎么办?

客户端是通过队列的 pop 操作来获取消息,然后进行处理。处理完了再接着获取消息,再进行处理。如此循环往复,这便是作为队列消费者的客户端的生命周期。

可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU,redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。

通常我们使用 sleep 来解决这个问题,让线程睡一会,睡个 1s 钟就可以了。不但客户端的 CPU 能降下来,Redis 的 QPS 也降下来了。

2.3 队列延迟

用上面睡眠的办法可以解决问题。但是有个小问题,那就是睡眠会导致消息的延迟增大。如果只有 1 个消费者,那么这个延迟就是 1s。如果有多个消费者,这个延迟会有所下降,因为每个消费者的睡觉时间是岔开来的。

有没有什么办法能显著降低延迟呢?你当然可以很快想到:那就把睡觉的时间缩短点。这种方式当然可以,不过有没有更好的解决方案呢?当然也有,那就是 blpop/brpop。这两个指令的前缀字符b代表的是blocking,也就是阻塞读。

阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。用blpop/brpop替代前面的lpop/rpop,就完美解决了上面的问题

Redis blpop 命令
Redis blpop 命令移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
Redis blpop 命令基本语法如下:

redis 127.0.0.1:6379> blpop list1 list2 .. listn  timeout

可用版本 Redis >= 2.0.0

返回值
如果列表为空,返回一个 nil 。 否则,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的 key ,第二个元素是被弹出元素的值。

实例
为了演示生产者和消费者,我们开两个Redis 客户端来操作。
客户端一:生产者

127.0.0.1:6379> rpush message-queue Java C++ Go
(integer) 3
127.0.0.1:6379> llen message-queue
(integer) 3

生产者生产了三条消息供消费者消费,现在我们操作客户端二:消费者

lpop message-queue
"Java"
127.0.0.1:6379> llen message-queue
(integer) 2
127.0.0.1:6379> lpop message-queue
"C++"
127.0.0.1:6379> lpop message-queue
"Go"
127.0.0.1:6379> lpop message-queue
(nil)
127.0.0.1:6379> lpop message-queue
(nil)

消费者轮询消费,即使消息队列中没有消息了也会一直去消费。现在我们将
lpop换成blpop来进行消费,客户端二:消费者

127.0.0.1:6379> blpop message-queue 5
(nil)
(5.08s)

等5秒后继续消费,依然没有消息,返回nil。现在我们把等等时间加长一点,改成20秒,然后操作客户端一去产生一条新消息,操作如下,客户端二:消费者

127.0.0.1:6379> blpop message-queue 20

客户端一:生产者

127.0.0.1:6379> rpush message-queue Swift
(integer) 1

客户端二:消费者 读到新生成的消息

127.0.0.1:6379> blpop message-queue 20
1) "message-queue"
2) "Swift"
(2.49s)
2.4 空闲连接自动断开

你以为上面的方案真的很完美么?先别急着开心,其实他还有个问题需要解决。什么问题?—— 空闲连接的问题。如果线程一直阻塞在哪里,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候blpop/brpop会抛出异常来。所以编写客户端消费者的时候要小心,注意捕获异常,还要重试。

三、位图

3.1 Redis位图基本概念

位图不是一个真实的数据类型,而是定义在字符串类型上的面向位的操作的集合。Redis中所有数据都是二进制形式存储的。Redis支持一个setbit和getbit操作,它支持在某个key的value上直接对某个二进制位操作,每个二进制位都只有0和1两种状态(默认为0),正好可以表示用户是否活跃两种状态。

Redis的位图可用于的场景有:
1、用户签到
2、用户每日在线状态
3、统计活跃用户数
4、各种状态值

说明:只适合做数据统计,不适合取具体值

3.2 常用命令

SETBIT
对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。

setbit key offset value

offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。

GETBIT
对 key 所储存的字符串值,获取指定偏移量上的位(bit)。

getbit key offset

BITCOUNT
计算给定字符串中,被设置为 1 的比特位的数量。

bitcount key

BITPOS
返回位图中第一个值为 bit 的二进制位的位置。

bitpos key bit [start] [end]

BITOP
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。

bitop operation destkey key [key …]

operation 可以是 and 、 or 、 not 、 xor 这四种操作中的任意一种
bitop and destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destroy 。

BITFIELD
bitfield 有三个子指令,分别是 get/set/incrby,它们都可以对指定位片段进行读写,但是最多只能处理 64 个连续的位,如果超过 64 位,就得使用多个子指令,bitfield 可以一次执行多个子指令。

3.3 位图应用于用户在线状态

位图操作是用来操作比特位的,其优点是节省内存空间。为什么可以节省内存空间呢?假如我们需要存储100万个用户的登录状态,使用位图的话最少只需要100万个比特位(比特位1表示登录,比特位0表示未登录)就可以存储了,而如果以字符串的形式存储,比如说以userId为key,是否登录(字符串“1”表示登录,字符串“0”表示未登录)为value进行存储的话,就需要存储100万个字符串了,相比之下使用位图存储占用的空间要小得多,这就是位图存储的优势。

具体操作: 写入一个key,值是一亿个bit,足够容纳你的数据中最大offset就行,假如这个key的名称是user-login-status

127.0.0.1:6379> setbit user-login-status-2019-12-12 20000 1
(integer) 0
127.0.0.1:6379> setbit user-login-status-2019-12-12 20001 0
(integer) 0
127.0.0.1:6379> getbit user-login-status-2019-12-12 20000
(integer) 1
127.0.0.1:6379> getbit user-login-status-2019-12-12 20001
(integer) 0
127.0.0.1:6379> getbit user-login-status-2019-12-12 20002
(integer) 0

1、设置id为20000的用户为登录状态
2、设置id为20001的用户为非登录状态
3、查看id为20000的用户的登录状态,返回值为1
4、查看id为20001的用户的登录状态,返回值为0
5、查看id为20002的用户的登录状态,返回值为0(默认为0)

3.4 位图应用于用户每日打卡

1、第一天2019-12-12打卡用户:20000、20001、20002

127.0.0.1:6379> setbit clock-in-2019-12-12 20000 1
(integer) 0
127.0.0.1:6379> setbit clock-in-2019-12-12 20001 1
(integer) 0
127.0.0.1:6379> setbit clock-in-2019-12-12 20002 1
(integer) 0

2、统计2019-12-12打卡用户数

127.0.0.1:6379> bitcount clock-in-2019-12-12
(integer) 3

3、第二天2019-12-13打卡用户:20001 、20002、20003

127.0.0.1:6379> setbit clock-in-2019-12-13 20001 1
(integer) 0
127.0.0.1:6379> setbit clock-in-2019-12-13 20002 1
(integer) 0
127.0.0.1:6379> setbit clock-in-2019-12-13 20003 1

4、统计某两天都有打卡的用户数

127.0.0.1:6379> bitop and result-and clock-in-2019-12-12 clock-in-2019-12-13
(integer) 2501
127.0.0.1:6379> bitcount result-and
(integer) 2

结果为2,即20001 、20002这两个用户在这两天都有打卡。注意,不用看2501。

5、统计某两天内总共的打卡人数

127.0.0.1:6379> bitop or result-or clock-in-2019-12-12 clock-in-2019-12-13
(integer) 2501
127.0.0.1:6379> bitcount result-or
(integer) 4

结果为4,即这两天打卡的用户是20000、20001、20002、20003,总共4人。

6、缺点
无法获取某一天所有的打卡用户信息。

四、HyperLogLog

4.1 用HyperLogLog 统计UV

我们先思考一个常见的业务问题:如果你负责开发维护一个大型的网站,有一天老板找产品经理要网站每个网页每天的 UV 数据,然后让你来开发这个统计模块,你会如何实现?

如果统计 PV 那非常好办,给每个网页一个独立的 Redis 计数器就可以了,这个计数器的 key 后缀加上当天的日期。这样来一个请求,incrby 一次,最终就可以统计出所有的 PV 数据。

但是 UV 不一样,它要去重,同一个用户一天之内的多次访问请求只能计数一次。这就要求每一个网页请求都需要带上用户的 ID,无论是登陆用户还是未登陆用户都需要一个唯一 ID 来标识。

你也许已经想到了一个简单的方案,那就是为每一个页面一个独立的 set 集合来存储所有当天访问过此页面的用户 ID。当一个请求过来时,我们使用 sadd 将用户 ID 塞进去就可以了。通过 scard 可以取出这个集合的大小,这个数字就是这个页面的 UV 数据。没错,这是一个非常简单的方案。

但是,如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,值得么?其实老板需要的数据又不需要太精确,105w 和 106w 这两个数字对于老板们来说并没有多大区别,So,有没有更好的解决方案呢?

这就是本节要引入的一个解决方案,Redis 提供了 HyperLogLog 数据结构就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。

4.2 HyperLogLog具体语法

HyperLogLog 提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值。

127.0.0.1:6379> pfadd index-uv user1
(integer) 1
127.0.0.1:6379> pfcount index-uv
(integer) 1
127.0.0.1:6379> pfadd index-uv user2
(integer) 1
127.0.0.1:6379> pfcount index-uv
(integer) 2
127.0.0.1:6379> pfadd index-uv user2
(integer) 0
127.0.0.1:6379> pfcount index-uv
(integer) 2
127.0.0.1:6379> pfadd index-uv user3 user4 user5 user6
(integer) 1
127.0.0.1:6379> pfcount index-uv
(integer) 6

简单试了一下,发现还蛮精确的,一个没多也一个没少。但如果往里面灌更多数据误差就会产生。

4.3 pfadd 这个 pf 是什么意思

它是 HyperLogLog 这个数据结构的发明人 Philippe Flajolet 的首字母缩写,老师觉得他发型很酷,看起来是个佛系教授。

4.4 pfmerge 适合什么场合用?

HyperLogLog 除了上面的 pfadd 和 pfcount 之外,还提供了第三个指令 pfmerge,用于将多个 pf 计数值累加在一起形成一个新的 pf 值。

比如在网站中我们有两个内容差不多的页面,运营说需要这两个页面的数据进行合并。其中页面的 UV 访问量也需要合并,那这个时候 pfmerge 就可以派上用场了。

4.5 HyperLogLog总结
  • Redis 在 2.8.9 版本添加了 HyperLogLog 结构
  • HyperLogLog是一种算法,并非Redis独有
  • 目的是做基数统计,故不是集合,不会保存元数据,只记录数量而不是数值。
  • 耗空间极小,支持输入非常体积的数据量
  • 核心是基数估算算法,主要表现为计算时内存的使用和数据合并的处理。最终数值存在一定误差
  • Redis中每个hyperloglog key占用了12K的内存用于标记基数(官方文档)
  • pfadd命令并不会一次性分配12k内存,而是随着基数的增加而逐渐增加内存分配;而pfmerge操作则会将sourcekey合并后存储在12k大小的key中,这由hyperloglog合并操作的原理(两个hyperloglog合并时需要单独比较每个桶的值)可以很容易理解。
  • 误差说明:基数估计的结果是一个带有 0.81% 标准错误(standard error)的近似值。是可接受的范围
  • Redis 对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间

五、布隆过滤器

使用 HyperLogLog 数据结构来进行估数,它非常有价值,可以解决很多精确度不高的统计需求。但是如果我们想知道某一个值是不是已经在 HyperLogLog 结构里面了,它就无能为力了,它只提供了 pfadd 和 pfcount 方法,没有提供 pfcontains 这种方法。

讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?

你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?

实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查询,当系统并发量很高时,数据库是很难扛住压力的。

你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不缓存的话,性能又跟不上,这该怎么办?

这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有一定的误判概率。

5.1 布隆过滤器是什么?

布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某个对象是否存在时,它可能会误判。但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。

当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。打个比方,当它说不认识你时,肯定就不认识;当它说见过你时,可能根本就没见过面,不过因为你的脸跟它认识的人中某脸比较相似 (某些熟脸的系数组合),所以误判以前见过你。

套在上面的使用场景中,布隆过滤器能准确过滤掉那些已经看过的内容,那些没有看过的新内容,它也会过滤掉极小一部分 (误判),但是绝大多数新内容它都能准确识别。这样就可以完全保证推荐给用户的内容都是无重复的。

5.2 Redis 中的布隆过滤器

Redis 官方提供的布隆过滤器到了 Redis 4.0 提供了插件功能之后才正式登场。布隆过滤器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能。安装省略

5.3 布隆过滤器基本使用

布隆过滤器有二个基本指令,bf.add 添加元素,bf.exists 查询元素是否存在,它的用法和 set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一个元素,如果想要一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需要用到 bf.mexists 指令。

127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.exists codehole user2
(integer) 1
127.0.0.1:6379> bf.exists codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user4
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0

资料来源:《Redis深度历险:核心原理与应用实践》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容

  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 17,036评论 1 51
  • Redis系列之进阶篇(上) 前言 这篇文章主要内容是: 分布式锁 延时队列 位图 HyperLogLog本文所学...
    可苯阅读 381评论 0 1
  • 原理分析 最近看到好多博主都在推分布式锁,实现方式很多,基于db、redis、zookeeper。zookeepe...
    ongahong阅读 680评论 0 0
  • 五种数据结构简介 Redis是使用C编写的,内部实现了一个struct结构体redisObject对象,通过结构体...
    彦帧阅读 6,934评论 0 14
  • 幼儿园观摩学习心得体会 周二下午随园长和几位老师观摩了**幼儿园,进入幼儿园首先映入眼帘的整洁舒适的校园环境,橡胶...
    姿一姿阅读 196评论 0 3