分布式锁
分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。
思考:小明问 如果执行业务逻辑的时候出现了异常程序终止,导致没有调用 del 指令,会发生什么,肿么办?
毫无疑问,坑被一直占用,陷入死锁,锁永远得不到释放。
于是我们可以在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。并且需要使用能保证 setnx 和 expire 可以组合在一起使用的原子指令。
思考:那,小明又问了,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。怎么办呢??
答:两个办法
-
Redis 分布式锁不要用于较长时间的任务,真出现了,人工解一下。
-
更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key。
可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。
延时队列
Redis 的 list(列表) 数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,使用 lpop 和 rpop 来出队列。
思考:队列空了怎么办?如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU,redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。
-
通常我们使用 sleep 来解决这个问题,让线程睡一会,睡个 1s 钟就可以了。不但客户端的 CPU 能降下来,Redis 的 QPS 也降下来了。
-
更好的,使用 阻塞读 blpop/brpop ,阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop,就完美解决了上面的问题。再配合空闲连接自动断开,减少闲置资源占用
布隆过滤器
布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”。
每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。
add:向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
exists:向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都位 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。
误识别率FPP公式的推导
假设布隆过滤器大小为m比特,存储了n个元素,使用k次散列函数来计算元素的存储位置。
-
添加1个元素,则任一比特为1的概率为:
1/m
,任一比特为0的概率:1-1/m
; -
添加1个元素,执行k次散列之后,则任一比特为0的概率:
(1-1/m)^k
,任一比特为1的概率:1-(1-1/m)^k
; -
如果添加n个元素,那么任一比特为0的概率:
(1-1/m)^kn
,任一比特为1的概率:1-(1-1/m)^kn
; -
如果将1个新的元素,添加到已存在n个元素的布隆过滤器中,则任一比特已经为1的概率与上面相同,概率为:
1-(1-1/m)^kn
。因此,k个比特都为1的概率为:(1-(1-1/m)^kn)^k
,此即为新插入元素的误识别率。
当n值比较大时,(1-(1-1/m)kn)k约等于:(1-e-kn/m)k
推导过程如下:
两个重要极限:
lim(x→∞)sinx/x=1
lim(x→0)(1+x)^1/x=e或 lim(x→∞)(1+1/x)^x=e (其中e=2.7182818...是一个无理数,也就是自然对数的底数)
首先,1的无穷大次方并不等于e,而是等于1。而, lim(x→∞)(1+1/x)^x=e,比如:1.0001已经很接近1了,但1.0001^10000却等于2.718145...远远大于1
布隆过滤器应用
在实际工作中,布隆过滤器常见的应用场景如下:
-
网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
-
反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
-
Google Chrome 使用布隆过滤器识别恶意 URL;
-
Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
-
Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。 除了上述的应用场景之外,布隆过滤器还有一个应用场景就是解决缓存穿透的问题。所谓的缓存穿透就是服务调用方每次都是查询不在缓存中的数据,这样每次服务调用都会到数据库中进行查询,如果这类请求比较多的话,就会导致数据库压力增大,这样缓存就失去了意义。
利用布隆过滤器我们可以预先把数据查询的主键,比如用户 ID 或文章 ID 缓存到过滤器中。当根据 ID 进行数据查询的时候,我们先判断该 ID 是否存在,若存在的话,则进行下一步处理。若不存在的话,直接返回,这样就不会触发后续的数据库查询。需要注意的是缓存穿透不能完全解决,我们只能将其控制在一个可以容忍的范围内。