redis做分布式锁
redis的实现方式其实就是占坑,一般使用setnx(set if not exists)指令,只允许一个客户端占坑。
setnx lock:codehole true
做其他事情
del lock:codehole
bug:如果del前有异常,导致del无法调用,就会使得锁得不到释放,就会死锁。
所以考虑给锁加时间,例如加5s,哪怕有异常,5s后也释放
setnx lock:codehole true
expire lock:codehole 5
做其他事情
del lock:codehole
bug1:set和expire不是原子操作,可能执行expire前出问题了,还是会死锁。而且这里不能使用事务来解决,例如线程A先用锁,线程B进来要锁,如果是事务,线程B没抢到锁,是不应该执行expire操作的。
为解决这个问题,redis2.8之后加入了set指令扩展
set lock:codehole true ex 5 nx
做其他事情
del lock:codehole
为了误删锁,建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断
可以借助Lua脚本实现,实现匹配随机数和删除锁的原子操作。
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
bug2:虽然解决了setnx和expire的问题,但是还有个超时的问题,线程A用时超过5s还没执行完,锁可能会被其他线程抢走
有个现成的方案,就是使用Redisson。Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
------
著作权归JavaGuide(javaguide.cn)所有
基于MIT协议
原文链接:https://javaguide.cn/distributed-system/distributed-lock-implementations.html
tips:不能给锁设置超时时间,不然不会出发看门狗。
锁冲突
其他线程没有获得锁怎么办:
1、直接抛出异常,通知稍后重试
2、sleep一会再重试
3、将请求转移到延时队列,过一会再试
redis数据过期
定时扫描策略
从库的过期策略
内存淘汰机制
redis做延时队列
适用于只有一组消费者的消息队列。redis做消息队列没有ack确认,不保证可靠性。
异步消息队列
1、用list列表,使用lpush/rpush、lpop/rpop来操作即可。
队列空了,就time.sleep()一会。
bug:如果有多个消费者,延时会加剧。采用阻塞读blocking。
使用blpush/brpush、blpop/brpop,阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。
bug:一直阻塞的话,会产生空闲连接的问题,服务器会主动断开连接,因此要注意捕获异常
2、为了改进不能多播的问题,提供了一个模块PubSub,就是PublisherSubscriber。
消费者:
// python
import time
import redis
client = redis.StrictRedis()
p = client.pubsub()
p.subscribe("codehole")
for msg in p.listen():
print msg
// 订阅多个主题
subscribe codehole.image codehole.text codehole.blog
psubscribe codehole.*
生产者:
import redis
client = redis.StrictRedis()
client.publish("codehole", "python comes")
client.publish("codehole", "java comes")
client.publish("codehole", "golang comes")
// 发布多个主题
publish codehole.image https://www.google.com/dudo.png
publish codehole.blog '{"content": "hello, everyone", "title": "welcome"}'
3、近期 Redis5.0 新增了 Stream 数据结构,这个功能给 Redis 带来了持久化消息队列,从此 PubSub 可以消失了。
支持多播的可持久化的消息队列Stream
它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。
消息ID:1527846880572-5。它表示当前的消息在毫米时间戳 1527846880572 时产生,并且是该毫秒内产生的第 5 条消息。消息 ID 可以 由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面 加入的消息的 ID 要大于前面的消息 ID。
延时队列的实现
通过zset有序列表实现。将消息作为key,消息的到期时间作为score。用多个线程轮询 zset 获取到期的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处理。因为有多个线程,所以需要考虑并发争抢任务,确保任务不能被多次执行。
位图
HyperLogLog
布隆过滤器
原理
空间估计
![](https://i-blog.csdnimg.cn/blog_migrate/47e32658c7f2761fcf26cb31e1e0a0b2.png)
限流
1、java计数器
Java内部也可以通过原子类计数器AtomicInteger
、Semaphore
信号量来做简单的限流。
// 限流的个数
private int maxCount = 10;
// 指定的时间内
private long interval = 60;
// 原子类计数器
private AtomicInteger atomicInteger = new AtomicInteger(0);
// 起始时间
private long startTime = System.currentTimeMillis();
public boolean limit(int maxCount, int interval) {
atomicInteger.addAndGet(1);
if (atomicInteger.get() == 1) {
startTime = System.currentTimeMillis();
atomicInteger.addAndGet(1);
return true;
}
// 超过了间隔时间,直接重新开始计数
if (System.currentTimeMillis() - startTime > interval * 1000) {
startTime = System.currentTimeMillis();
atomicInteger.set(1);
return true;
}
// 还在间隔时间内,check有没有超过限流的个数
if (atomicInteger.get() > maxCount) {
return false;
}
return true;
}
2、redis做限流
zipList:
满足以下两个条件:
- [score,value]键值对数量少于128个;
- 每个元素的长度小于64字节;
skipList:
不满足以上两个条件时使用跳表(组合了hash和skipList)
- hash用来存储value到score的映射,这样就可以在O(1)时间内找到value对应的分数;
- skipList按照从小到大的顺序存储分数;
- skipList每个元素的值都是[score,value]对
简单限流原理
- 每次请求时,将当前时间戳作为 score,唯一标识符(如用户ID+请求类型)作为 member 添加到 Sorted Set 中。
- 移除时间戳早于当前时间减去窗口大小的所有元素。
- 检查 Sorted Set 的大小是否超过了限制。
public class SimpleRateLimiter {
private Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey);
// 时间戳
long nowTs = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
// 记录行为 value和score都是毫秒时间戳
pipe.zadd(key, nowTs, "" + nowTs);
// 移除时间窗口之前的行为记录 0-nowTs-60*1000分数范围删除元素
pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
// 统计窗口内的行为数量
Response<Long> count = pipe.zcard(key);
// 设置用户的过期时间61s,避免冷用户占用内存,宽限多1s
pipe.expire(key, period + 1);
// 执行
pipe.exec();
pipe.close();
// 比较是否超标
return count.get() <= maxCount;
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
for(int i=0;i<20;i++) {
// 60s内超5次就限流
System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
}
}
}
漏斗算法
漏桶算法思路很简单,我们把水比作是请求
,漏桶比作是系统处理能力极限
,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。
- 请求的流入:当请求到达时,它们被看作是流入桶中的水。
- 桶的容量:桶有一个固定的容量,这代表了系统能够处理的请求的最大数量。
- 漏出速率:桶底部有一个漏洞,水(请求)以固定的速率从桶中漏出,这个速率代表了系统处理请求的速率。
- 流量整形:如果桶满了,新流入的水(请求)会被丢弃,直到桶中有足够的空间。
![](https://i-blog.csdnimg.cn/blog_migrate/d5e5338a32fd225b05afe57add97eb6f.png)
令牌桶
- 令牌生成:系统以固定的速率向桶中添加令牌,这个速率通常由网络策略或API限制决定。
- 桶的容量:桶有一个最大容量,当桶满时,新生成的令牌会被丢弃。
- 请求处理:当一个数据包或请求到达时,它需要从桶中取出一个令牌才能被处理。如果桶中有足够的令牌,请求可以立即被处理;如果桶空了,则请求必须等待,直到桶中再次有令牌可用。
- 突发传输:由于桶可以存放一定数量的令牌,系统可以在短时间内处理等于桶容量大小的突发流量,而不会因为短暂的流量高峰而完全阻塞。
scan
![](https://i-blog.csdnimg.cn/blog_migrate/f9e88a1d01efb042047659cb6114b580.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f5fd6b193eb9f306e44d1b75d7b551f2.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2ca9c64fe9474ab1ea068304e838e6c2.png)
大key扫描
定位大key
线程IO
Redis是一个单线程程序
为什么redis单线程也能跑这么快?
因为数据都存在内存中,运算都是内存级别的运算,对于On级别的指令一定要小心谨慎!避免造成redis卡顿
redis单线程如何处理并发请求?
多路复用、事件轮询、非阻塞IO(NIO)
非阻塞IO(NIO)
在套接字对象上提供了一个Non_Blocking,这个选项打开,意味着读写方法不会阻塞,变成能读多少读多少,能写多少写多少。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。
事件轮询
非阻塞IO有个bug,线程读数据,读了一部分就返回了,它怎么知道什么时候继续读。事件轮询API就是为了解决这个问题。
输入是读写描述符列表 read_fds & write_fds,输出是与之对应的可读可写事件。
同时还提供了一个 timeout 参数,如果没有任何事件到来,那么就最多等待 timeout 时间,线程处于阻塞状态。
拿到事件后,线程就可以继续挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期。
因为我们通过 select 系统调用同时处理多个通道描述符的读写事件,因此我们将这类系统调用称为多路复用 API。现代操作系统的多路复用 API 已经不再使用 select 系统调用,而改用epoll(linux)和 kqueue(freebsd & macosx)
定时任务
redis的定时任务会记录在一个称为最小堆的数据结构中。这个堆中,最快要执行的任务排在堆的最上方。在每个循环周期, redis都会将最小堆里面已经到点的任务立即进行处理。处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是select系统调用的timeout参数。因为知道未来时间内,没有其它定时任务需要处理,所以可以安心睡眠 timeout的时间。
通讯协议
![](https://i-blog.csdnimg.cn/blog_migrate/cc1f585d90d27fcea1a6615d85d3fc9b.png)
持久化
Redis 的持久化机制有两种,第一种是快照,第二种是 AOF 日志。
快照原理
快照要求redis进行IO操作,但IO操作是不能进行多路复用API的。这因为着如果正在持久化一个大的字典,来了个请求把这个持久化打断了,这就白干了。
因此reids使用操作系统的多进程COW(Copy On Write)机制来实现快照。
持久化的时候调用glibc的函数fork产生一个子线程,快照任务交给子进程来解决,父进程继续处理客户端请求。子进程刚产生的时候,共享父进程的代码段和数据段。
子进程只负责将它有的所有数据序列化到磁盘里,不产生新数据。但是父进程一直在处理客户端请求,会对内存的数据进行不间断的修改。
这个时候就要用COW机制来实现数据段页面的分离。其实就是将共享的页面复制一份出来,父进程对这个复制出来的页面进行修改,子进程的数据还是那些。
AOP原理
AOF重写
- AOF 重写机制,它可以压缩和优化 AOF 文件的内容,减少冗余和无效的命令,提高数据的存储效率和恢复速度 。
- AOF 重写机制的原理是根据 Redis 进程内的数据生成一个新的 AOF 文件,只包含当前有效和存在的数据的写入命令,而不是历史上所有的写入命令 。
- AOF 重写机制是通过 fork 出一个子进程来完成的,子进程会扫描 Redis 的数据库,并将每个键值对转换为相应的写入命令,然后写入到一个临时文件中 。
- 在子进程进行 AOF 重写的过程中,主进程还会继续接收和处理客户端的请求,如果有新的写操作发生,主进程会将这些写操作追加到一个缓冲区中,并通过管道通知子进程 。
- 子进程在完成 AOF 重写后,会将缓冲区中的写操作也追加到临时文件中,然后向主进程发送信号,通知主进程可以切换到新的 AOF 文件了 。
- 主进程在收到子进程的信号后,会将缓冲区中的写操作再次追加到临时文件中(以防止在此期间有新的写操作发生),然后用临时文件替换旧的 AOF 文件,并关闭旧的 AOF 文件 。
混合持久化
快照和AOF日志一起使用。
AOF只存储快照持久化开始到持久化结束中间发生的增量AOF日志。
主从同步
CAP原理
C--一致性、A--可用性、P--分区容忍性
Redis的主从同步是异步同步的,所以分布式的Redis系统不满足一致性要求。
主从同步和从从同步
![](https://i-blog.csdnimg.cn/blog_migrate/d803e3e4eb9417c3e659d0d85082c197.png)
增量同步
快照同步
无盘复制
--主结点不是一次生成快照文件
Sentinel哨兵
为了避免主节点挂掉,可以将从节点变成主节点。
![](https://i-blog.csdnimg.cn/blog_migrate/0ce7549c300b43b4e358ed5de96f0b29.png)