中间件使用
- 一、Redis系列
- 二、RabbitMQ
- 1、为什么要使用MQ
- 2、什么是`RabbitMQ`
- 3、`RabbitMQ`各组件的功能
- 4、RabbitMQ工作原理
- 5、RabbitMQ 上的一个 queue 中存放的 message 是否有数量限制?
- 6、RabbitMQ 允许发送的 message 最大可达多大?
- 7、RabbitMQ的工作模式
- 8、如何保证RabbitMQ消息的顺序性?
- 9、RabbitMQ消息丢失的情况有哪些?
- 10、RabbitMQ如何保证消息不丢失?
- 11、RabbitMQ消息基于什么传输?
- 12、RabbitMQ支持消息的幂等性吗?
- 13、RabbitMQ怎么确保消息已被消费?
- 14、RabbitMQ支持事务消息吗?
- 15、RabbitMQ消息持久化的条件?
- 16、RabiitMQ消息什么情况下会变成死信消息?
- 17、RabbitMQ死信消息的来源?
- 18、RabbitMQ死信队列的用处?
- 19、RabbitMQ支持延迟队列吗?
- 20、RabbitMQ延迟队列的使用场景
- 21、RabbitMQ实现延迟队列的有什么条件?
- 三、flowable
- 架构
- 设计原理
- 1、 **Flowable是什么?请介绍一下其核心特点和优势**
- 2、**什么是BPMN?Flowable支持哪些版本的BPMN规范?**
- 3、**请对比一下Flowable与Activiti的异同点?**
- 4、**请列举一些常用的flowable API,以及它们分别的作用?**
- 5、**请说明一下flowable中流程监听器的作用,以及有哪些类型的监听器?**
- 6、**请解释一下什么是流程实例和执行对象,在flowable中有什么关系?**
- 7、**请说明一下flowable中流程定义的基本组成部分,以及这些组成部分的作用。**
- 8、**如何使用Flowable创建和部署一个流程定义?**
- 9、**如何启动、暂停、删除、挂起某个具体的流程实例?**
- **10、在Flowable中,如何查询并处理当前待办的任务?**
- 11、**如何使用Flowable实现子流程的调用和管理?**
一、Redis系列
Redis之所以备受青睐,原因如下:
- 快速读写性能: Redis将数据存储在内存中,因此能够实现高速的读写操作,是传统数据库的数倍甚至更高。
- 多样数据结构: Redis不仅支持简单的键值存储,还提供了丰富的数据结构,可以方便地存储和处理复杂的数据。
- 持久化支持: Redis支持数据持久化,可以将内存中的数据保存到磁盘,确保数据不会因为系统重启而丢失。
- 分布式支持: Redis的分布式特性使得它可以构建高可用、高可扩展的应用系统。
1.redis为什么快?
1)完全基于内存操作
(2)数据结构简单,对数据操作简单
(3)redis执行命令是单线程的,避免了上下文切换带来的性能问题,也不用考虑锁的问题
(4) 采用了非阻塞的io多路复用机制,使用了单线程来处理并发的连接;内部采用的epoll+自己实现的事件分离器
其实Redis不是完全多线程的,在核心的网络模型中是多线程的用来处理并发连接,但是数据的操作都是单线程。Redis坚持单线程是因为Redis是的性能瓶颈是网络延迟而不是CPU,多线程对数据读取不会带来性能提升。
2、redis持久化机制
(1)快照持久化RDB
redis的默认持久化机制,通过父进程fork一个子进程,子进程将redis的数据快照写入一个临时文件,等待持久化完毕后替换上一次的rdb文件。整个过程主进程不进行任何的io操作。持久化策略可以通过save配置单位时间内执行多少次操作触发持久化。所以RDB的优点是保证redis性能最大化,恢复速度数据较快,缺点是可能会丢失两次持久化之间的数据
(2)追加持久化AOF
以日志形式记录每一次的写入和删除操作,策略有每秒同步、每次操作同步、不同步,优点是数据完整性高,缺点是运行效率低,恢复时间长
3、Redis如何实现key的过期删除?
采用的定期过期+惰性过期
定期删除 :Redis 每隔一段时间从设置过期时间的 key 集合中,随机抽取一些 key ,检查是否过期,如果已经过期做删除处理。
惰性删除 :Redis 在 key 被访问的时候检查 key 是否过期,如果过期则删除。
4、Redis数据类型应用场景
String:可以用来缓存json信息,可以用incr命令实现自增或自减的计数器
Hash:与String一样可以保存json信息
List:可以用来做消息队列,list的pop是原子性操作能一定程度保证线程安全
Set:可以做去重,比如一个用户只能参加一次活动 ;可以做交集求共友
SortSet :有序的。可以实现排行榜
5、Redis缓存穿透如何解决?
缓存穿透是指频繁请求客户端和缓存中都不存在的数据,缓存永远不生效,请求都到达了数据库。
解决方案:
(1)在接口上做基础校验,比如id<=0就拦截
(2)缓存空对象:找不到的数据也缓存起来,并设置过期时间,可能会造成短期不一致
(3)布隆过滤器:在客户端和缓存之间添加一个过滤器,拦截掉一定不存在的数据请求
6、Redis如何解决缓存击穿?
缓存击穿是值一个key非常热点,key在某一瞬间失效,导致大量请求到达数据库
解决方案:
(1)设置热点数据永不过期
(2)给缓存重建的业务加上互斥锁,缺点是性能低
7、Redis如何解决缓存雪崩?
缓存雪崩是值某一时间Key同时失效或redis宕机,导致大量请求到达数据库
解决方案:
(1)搭建集群保证高可用
(2)进行数据预热,给不同的key设置随机的过期时间
(3)给缓存业务添加限流降级,通过加锁或队列控制操作redis的线程数量
(4)给业务添加多级缓存
8、Redis分布式锁的实现原理
原理是使用setnx+setex命令来实现,但是会有一系列问题:
(1)任务时常超过缓存时间,锁自动释放。可以使用Redision看门狗解决
(2)加锁和释放锁的不是同一线程。可以在Value中存入uuid,删除时进行验证。但是要注意验证锁和删除锁也不是一个原子性操作,可以用lua脚本使之成为原子性操作
(3)不可重入。可以使用Redision解决(实现机制类似AQS,计数)
(4)redis集群下主节点宕机导致锁丢失。使用红锁解决
Redision
1.Redisson的引入
1、我们先看看之前基于setnx实现的分布式锁存在的问题: 不可重入:同一个线程无法多次获取同一把锁 不可重试:获取锁只尝试一次就返回false,没有重试机制 超时释放:锁超时释放虽然避免死锁,但是业务执行耗时较长,也会导致释放存在安全隐患 主从一致:如果redis提供了主从集群,主从同步存在延迟,当主节点宕机,如果从节点同步主中的锁数据,则会出现锁失效
2、什么Redisson?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
3、Redisson使用手册
Redission使用手册https://www.bookstack.cn/read/redisson-wiki-zh/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D.md里面提到了Redisson可以实现大致如下的分布式锁
4、Redisson快速入门
<!-- redis-redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
/**
* 配置 Redisson
*/
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.20:6379").setPassword("123456");
// 创建 RedissonClient 对象
return Redisson.create(config);
}
}
@Test
void testRedisson() throws Exception {
RLock anyLock = redissonClient.getLock("anyLock");
boolean isLock = anyLock.tryLock(1, 10, TimeUnit.SECONDS);
if(isLock)
try
System.out.println("执行业务");
finally
anyLock.unlock();
}
5、redision的加锁解锁流程
优点
-
互斥性。在任意时刻,只有一个客户端能持有锁,也叫唯一性。
-
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
-
防误删。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。(业务执行时间过长,超过锁失效时间,锁被释放,第二个线程获取锁,此时第一个线程执行到释放锁代码时,不能删除第二个线程的锁)
-
看门狗机制。延长过期时间(没有设置过期时间的情况,leaseTime=-1,默认失效时间为30秒,启动看门狗线程,定时检查是否需要延长时间scheduleExpirationRenewal)。
-
具有可用性、容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。
-
可重入性。相同线程不需要在等待锁,而是可以直接进行相应操作
-
锁种类多样。可重入锁、公平锁、联锁、红锁、读写锁
-
可阻塞等待。
存在的问题
分布式架构中的CAP理论,分布式系统只能同时满足两个: 一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)
Redisson分布式锁是AP模式,当锁存在的redis节点宕机,可能会被误判为锁失效,或者没有加锁。(Zookeeper实现的分布式锁,是CP理论)
因为在工作中Redis都是集群部署的,所以要考虑集群节点挂掉的问题。给大家举个例子:
- A客户端请求主节点获取到了锁
- 主节点挂掉了,但是还没把锁的信息同步给其他从节点
- 由于主节点挂了,这时候开始主从切换,从节点成为主节点继续工作,但是新的主节点上,没有A客户端的加锁信息
- 这时候B客户端来加锁,因为目前是一个新的主节点,上面没有其他客户端加锁信息,所以B客户端获取锁成功
- 这时候就存在问题了,A和B两个客户端同时都持有锁,同时在执行代码,那么这时候分布式锁就失效了。
解决办法:
tradoff,分布式锁的redis采用单机部署,分布式锁专用
RedLock: RedLock算法思想,意思是不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,n / 2 + 1,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免说仅仅在一个redis实例上加锁而带来的问题。
如果对锁比较关注,一致性要求比较高,可以使用ZK实现的分布式锁
红锁
1:介绍
红锁本质上就是使用多个Redis做锁。例如有5个Redis,一次锁的获取,会对每个请求都获取一遍,如果获取锁成功的数量超过一半(2.5),则获取锁成功,反之失败;
释放锁也需要对每个Redis释放
2、红锁原理
- 在Redis的分布式环境中,我们假设有5个Redis master。这些节点完全互相独立,没有主从关系
- 线程1向这5个redis加锁,当加到第三个的时候,4和5加不上了;但是符合红锁的n/2+1原则,所以线程1获取到了锁;
- 当redis3挂了,此时线程1获取到了锁,正在顺序执行,
- 线程2来到了redis抢占锁,因为3挂了,1,2有锁,只有4和5可以加锁,因为我们注册的时候是5台,4和5这两台不满足n/2+1原则,抢占锁失败;
3、红锁使用说明-官网介绍
基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
大家都知道,如果负责储存某些分布式锁的某些Redis节点宕机以后,而且这些锁正好处于锁住的状态时,这些锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
4、红锁实战
注册红锁的RedissonClient
@Component
public class RedisConfig {
@Bean(name = "redissonRed1")
@Primary
public RedissonClient redissonRed1(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed2")
public RedissonClient redissonRed2(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6380").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed3")
public RedissonClient redissonRed3(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6381").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed4")
public RedissonClient redissonRed4(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6382").setDatabase(0);
return Redisson.create(config);
}
@Bean(name = "redissonRed5")
public RedissonClient redissonRed5(){
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6383").setDatabase(0);
return Redisson.create(config);
}
}
红锁使用
package com.online.taxi.order.service.impl;
import com.online.taxi.order.constant.RedisKeyConstant;
import com.online.taxi.order.service.GrabService;
import com.online.taxi.order.service.OrderService;
import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class GrabRedisRedissonRedLockLockServiceImpl implements GrabService {
// 红锁
@Autowired
@Qualifier("redissonRed1")
private RedissonClient redissonRed1;
@Autowired
@Qualifier("redissonRed2")
private RedissonClient redissonRed2;
@Autowired
@Qualifier("redissonRed3")
private RedissonClient redissonRed3;
@Autowired
@Qualifier("redissonRed4")
private RedissonClient redissonRed4;
@Autowired
@Qualifier("redissonRed5")
private RedissonClient redissonRed5;
@Autowired
OrderService orderService;
@Override
public String grabOrder(int orderId , int driverId){
System.out.println("红锁实现类");
//生成key
String lockKey = ("" + orderId).intern();
//redisson锁 单节点
// RLock rLock = redissonRed1.getLock(lockKey);
//红锁 redis son
RLock rLock1 = redissonRed1.getLock(lockKey);
RLock rLock2 = redissonRed2.getLock(lockKey);
RLock rLock3 = redissonRed3.getLock(lockKey);
RLock rLock4 = redissonRed4.getLock(lockKey);
RLock rLock5 = redissonRed5.getLock(lockKey);
RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3,rLock4,rLock5);
try {
/**红锁
* waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
* leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
*/
boolean b1 = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
if (b1){
System.out.println("加锁成功");
// 此代码默认 设置key 超时时间30秒,过10秒,再延时
System.out.println("司机:"+driverId+" 执行抢单逻辑");
boolean b = orderService.grab(orderId, driverId);
if(b) {
System.out.println("司机:"+driverId+" 抢单成功");
}else {
System.out.println("司机:"+driverId+" 抢单失败");
}
System.out.println("加锁成功");
}else {
System.out.println("加锁失败");
}
} finally {
rLock.unlock();
}
return null;
}
}
9、Redis集群方案
(1)主从模式:个master节点,多个slave节点,master节点宕机slave自动变成主节点
(2)哨兵模式:在主从集群基础上添加哨兵节点或哨兵集群,用于监控master节点健康状态,通过投票机制选择slave成为主节点
(3)分片集群:主从模式和哨兵模式解决了并发读的问题,但没有解决并发写的问题,因此有了分片集群。分片集群有多个master节点并且不同master保存不同的数据,master之间通过ping相互监测健康状态。客户端请求任意一个节点都会转发到正确节点,因为每个master都被映射到0-16384个插槽上,集群的key是根据key的hash值与插槽绑定
10、Redis集群主从同步原理
主从同步第一次是全量同步:slave第一次请求master节点会根据replid判断是否是第一次同步,是的话master会生成RDB发送给slave。
后续为增量同步:在发送RDB期间,会产生一个缓存区间记录发送RDB期间产生的新的命令,slave节点在加载完后,会持续读取缓存区间中的数据
11、Redis缓存一致性解决方案
Redis缓存一致性解决方案主要思考的是删除缓存和更新数据库的先后顺序
先删除缓存后更新数据库存在的问题是可能会数据不一致,一般使用延时双删来解决,即先删除缓存,再更新数据库,休眠X秒后再次淘汰缓存。第二次删除可能导致吞吐率降低,可以考虑进行异步删除。
先更新数据库后删除缓存存在的问题是会可能会更新失败,可以采用延时删除。但由于读比写快,发生这一情况概率较小。
但是无论哪种策略,都可能存在删除失败的问题,解决方案是用中间件canal订阅binlog日志提取需要删除的key,然后另写一段非业务代码去获取key并尝试删除,若删除失败就把删除失败的key发送到消息队列,然后进行删除重试。
12、Redis内存淘汰策略
当内存不足时按设定好的策略进行淘汰,策略有(1)淘汰最久没使用的(2)淘汰一段时间内最少使用的(3)淘汰快要过期的
13、应用场景
- 缓存: 作为高性能缓存,加速热门数据的读取,减轻数据库压力。
- 会话存储: 存储用户会话信息,支持快速的用户认证和状态管理。
- 排行榜和计数器: 统计热门内容、用户活跃度等数据,快速计算排名和增减计数。
- 消息队列: 作为轻量级消息队列,支持任务异步处理和解耦系统组件。
14、Redis 用作缓存时
1 作为缓存使用流程
在典型的缓存场景中,应用程序首先检查 Redis 缓存中是否存在所需数据。如果缓存命中(数据存在于 Redis 中),则直接从 Redis 获取数据,避免了昂贵的数据库查询。如果缓存未命中,则从数据库中读取数据,并将数据缓存在 Redis 中,以便后续快速访问。
2 数据一致性问题
在使用 Redis 作为缓存时,需要考虑数据的一致性问题。数据一致性指的是在缓存与数据库之间保持数据的同步,避免数据出现不一致的情况。以下是常见的数据一致性策略:
2.1 先更新缓存,再更新数据库
在这种策略中,应用程序首先更新 Redis 缓存中的数据,然后再更新数据库。这样确保了数据先在缓存中更新,后在数据库中更新,减少了数据不一致的可能性。
2.2 先更新数据库,再更新缓存
这种策略与上述相反,应用程序首先更新数据库中的数据,然后再更新 Redis 缓存。这样做的好处是,在数据库更新失败的情况下,可以避免缓存中的数据与数据库不一致。
2.3 先删除缓存,再更新数据库
在执行更新操作之前,先删除 Redis 缓存中的对应数据。这样在下次读取数据时,强制从数据库中获取最新数据并更新到缓存中。
但如果是处于读写并发的情况下,还是会出现数据不一致的情况:
2.4 先更新数据库,再删除缓存
这种策略类似于先更新数据库再更新缓存,但在更新数据库成功后,再删除 Redis 缓存中的数据,确保缓存中的数据被删除,不再返回过期或无效的数据。
和之前一样,如果两段代码都执行成功,在并发情况下会是什么样呢?
3 数据一致性保障的最佳实践
3.1 延迟双删
在数据一致性方面,常常会出现"延迟双删"的问题。当数据在数据库更新后,立即删除 Redis 缓存可能会导致缓存中的数据在短时间内不一致。为了避免这种情况,可以采取延迟双删策略,即在更新数据库后,延迟一段时间再删除缓存,以确保数据已经在应用中稳定使用。
延迟双删的流程图:
3.2 通过发送MQ,在消费者线程去同步
Redis 为了更好地控制缓存与数据库的同步过程,可以使用消息队列(MQ)来处理缓存更新。在数据更新后,应用程序发送消息到 MQ,由消费者线程负责更新 Redis 缓存。这种异步方式避免了直接操作 Redis 导致的性能损耗,提高了系统的整体响应速度。
3.3 Canal 订阅日志实现
Canal 是阿里巴巴开源的一款基于数据库日志解析的工具,它可以订阅数据库的 binlog,并将数据变更事件转发给消息队列。通过 Canal,可以将数据库更新操作实时同步到 Redis 缓存,从而保持数据的一致性。
订阅数据库变更日志,当数据库发生变更时,我们可以拿到具体操作的数据,然后再去根据具体的数据,去删除对应的缓存。
3.4 使用事务
对于需要保证数据库与缓存数据完全一致性的场景,可以使用事务来处理数据更新。在更新数据库的同时,使用 Redis 事务来更新缓存,确保两者的操作要么同时成功,要么同时失败,从而维持数据的一致性。
3.5 采用乐观锁机制
乐观锁机制是一种较为轻量级的数据同步策略。在读取数据时,先获取数据的版本号,然后在更新数据时,检查版本号是否一致,如果一致才进行更新。这样可以避免并发更新导致的数据不一致问题。
二、RabbitMQ
1、为什么要使用MQ
1、流量消峰
举个例子:如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
2、应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
3、异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式:
- A 过一段时间去调用 B 的查询 api 查询
- A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。
这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。
2、什么是RabbitMQ
RabbitMQ
是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ
是一个快递站,一个快递员帮你传递快件。RabbitMQ
与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
3、RabbitMQ
各组件的功能
- Server:接收客户端的连接,实现
AMQP
实体服务。 - Connection:连接,应用程序与Server的网络连接,TCP连接。
- Channel:信道,消息读写等操作在信道中进行。客户端可以建立多个信道,每个信道代表一个会话任务。如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的
Connection极大减少了操作系统建立TCP connection****的开销 - Message:消息,应用程序和服务器之间传送的数据,消息可以非常简单,也可以很复杂。由Properties和Body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body就是消息体内容。
- Virtual Host:虚拟主机,用于逻辑隔离。一个虚拟主机里面可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名称的Exchange或Queue。
- Exchange:交换器,接收消息,按照路由规则将消息路由到一个或者多个队列。如果路由不到,或者返回给生产者,或者直接丢弃。
RabbitMQ
常用的交换器常用类型有direct、topic、fanout、headers四种,后面详细介绍。 - Binding:绑定,交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个
RoutingKey
,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据 RoutingKey
:路由键,生产者将消息发送给交换器的时候,会发送一个RoutingKey
,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。路由键通常为一个“.”分割的字符串,例如“com.rabbitmq”
。- Queue:消息队列,用来保存消息,供消费者消费。
4、RabbitMQ工作原理
不得不看一下经典的图了,如下
AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:
- 生产者是连接到 Server,建立一个连接,开启一个信道。
- 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。
- 消费者也需要进行建立连接,开启信道等操作,便于接收消息。
- 生产者发送消息,发送到服务端中的虚拟主机。
- 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。
- 订阅了消息队列的消费者就可以获取到消息,进行消费。
5、RabbitMQ 上的一个 queue 中存放的 message 是否有数量限制?
可以认为是无限制,因为限制取决于机器的内存,但是消息过多会导致处理效率的下降。
6、RabbitMQ 允许发送的 message 最大可达多大?
根据 AMQP 协议规定,消息体的大小由 64-bit 的值来指定,所以你就可以知道到底能发多大的数据了
7、RabbitMQ的工作模式
1、simple模式(即最简单的收发模式)
- 消息产生消息,将消息放入队列
- 消息的消费者(consumer) 监听 消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除(隐患 消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失,这里可以设置成手动的ack,但如果设置成手动ack,处理完后要及时发送ack消息给队列,否则会造成内存溢出)。
2、Work Queues(工作队列)
消息产生者将消息放入队列消费者可以有多个,消费者1,消费者2同时监听同一个队列,消息被消费。C1 C2共同争抢当前的消息队列内容,谁先拿到谁负责消费消息(隐患:高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个开关(syncronize) 保证一条消息只能被一个消费者使用)。
3、publish/subscribe发布订阅(共享资源)
- 每个消费者监听自己的队列;
- 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。
4、routing路由模式
- 消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息;
- 根据业务功能定义路由字符串
- 从系统的代码逻辑中获取对应的功能字符串,将消息任务扔到对应的队列中。
- 业务场景:error 通知;EXCEPTION;错误通知的功能;传统意义的错误通知;客户通知;利用key路由,可以将程序中的错误封装成消息传入到消息队列中,开发者可以自定义消费者,实时接收错误;
5、topic 主题模式(路由模式的一种)
- 星号井号代表通配符
- 星号代表多个单词,井号代表一个单词
- 路由功能添加模糊匹配
- 消息产生者产生消息,把消息交给交换机
- 交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费
8、如何保证RabbitMQ消息的顺序性?
- 拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue(消息队列)而已,确实是麻烦点;
- 或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
9、RabbitMQ消息丢失的情况有哪些?
- 生产者发送消息RabbitMQ Server 消息丢失
- RabbitMQ Server中存储的消息丢失
- RabbitMQ Server中存储的消息分发给消费者者丢失
1、生产者发送消息RabbitMQ Server 消息丢失
- 发送过程中存在网络问题,导致消息没有发送成功
- 代码问题,导致消息没发送
2、RabbitMQ Server中存储的消息丢失
消息没有持久化,服务器重启导致存储的消息丢失
3、RabbitMQ Server到消费者消息丢失
- 消费端接收到相关消息之后,消费端还没来得及处理消息,消费端机器就宕机了
- 处理消息存在异常
10、RabbitMQ如何保证消息不丢失?
针对上面的情况,确保消息不丢失
1、生产者发送消息RabbitMQ Server 消息丢失解决方案:
- 常用解决方案:发送方确认机制(publisher confirm)
- 开启AMQP的事务处理(不推荐)
2、RabbitMQ Server中存储的消息丢失解决方案:
- 消息回退:通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者
- 设置持久化:保证重启过程中,交换机和队列也是持久化的
3、RabbitMQ Server到消费者消息丢失解决方案:
- 手动ack确认机制
11、RabbitMQ消息基于什么传输?
由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。
12、RabbitMQ支持消息的幂等性吗?
支持。在消息生产时,MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列。
在消息消费时,要求消息体中必须要有一个 bizId(对于同一业务全局唯一,如支付 ID、订单 ID、帖子 ID 等)作为去重的依据,避免同一条消息被重复消费。
13、RabbitMQ怎么确保消息已被消费?
- 消费端配置手动ACK确认机制
- 结合数据库进行状态标记处理
14、RabbitMQ支持事务消息吗?
支持事务消息。前面在第9题中保证生产者不丢失消息,提到可以使用AMQP的事务,但是它是同步的,所以不怎么推荐使用
事务的实现主要是对信道(Channel)的设置,主要方法如下: 1. channel.txSelect() 声明启动事务模式 2.channel.txCommit() 提交事务 3.channel.txRollback()回滚事务
15、RabbitMQ消息持久化的条件?
消息持久化,当然前提是队列必须持久化
- 声明队列必须设置持久化 durable 设置为 true.
- 消息推送投递模式必须设置持久化,deliveryMode 设置为 2(持久)。
- 消息已经到达持久化交换器。
- 消息已经到达持久化队列。
16、RabiitMQ消息什么情况下会变成死信消息?
由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信消息
17、RabbitMQ死信消息的来源?
- 消息 TTL 过期
- 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
- 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
18、RabbitMQ死信队列的用处?
- 可以用于实现延迟队列
19、RabbitMQ支持延迟队列吗?
支持。延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
20、RabbitMQ延迟队列的使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒
- 用户注册成功后,如果三天内没有登陆则进行短信提醒
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
21、RabbitMQ实现延迟队列的有什么条件?
- 消息设置TTL
- 配置了死信队列
三、flowable
Flowable是一个基于Java的开源BPM框架,它主要基于Activiti中的一些组件,并在此基础上进行了扩展和升级。以下是Flowable的设计原理及架构解析:
架构
- Flowable架构主要分为四部分:工作流引擎、应用程序接口(API)、模型器和任务表单设计器。
- 工作流引擎:Flowable的核心组件,包括运行时引擎和执行引擎。它管理整个流程的生命周期,监控、控制任务的执行以及记录流程实例的状态等信息。
- 应用程序接口(API):根据RESTful风格,提供给外部系统访问Flowable引擎的接口,可以通过编写调用API的客户端程序来使用Flowable引擎服务。
- 模型器:用于创建和修改流程定义文件,支持基于Web的图形化编辑器。
- 任务表单设计器:用于创建和修改任务表单,支持基于Web的表单设计器。
设计原理
Flowable从设计层面遵循以下原则:
- 可扩展性(Extensibility):Flowable允许用户对平台进行扩展,可以使用自定义的Java类和JavaScript脚本添加新功能。
- 易用性(Usability):Flowable提供易于使用的编排和操作工具,用户可以快速创建、部署和管理业务流程。
- 平台独立性(Platform In-dependency):Flowable在设计时避免了与底层系统相关的代码,从而实现平台独立性。
总体来说,Flowable是一个基于Java语言、可扩展的BPM框架。其核心设计原则是可扩展性、易用性和平台独立性这三个方面。用户可以使用Flowable提供的工具,轻松构建、操作并监视高效的工作流程。
1、 Flowable是什么?请介绍一下其核心特点和优势
Flowable是一个开源的工作流引擎,它可以部署在Java平台上,支持BPMN 2.0标准,以及CMMN和DMN标准。它具有以下核心特点和优势:
-
可扩展性:Flowable可以轻松地与其他应用程序集成,例如Spring和Hibernate等,以实现更高级的工作流管理。
-
云原生:Flowable是云原生工作流引擎,可以轻松适应不同的云环境,并支持基于容器的部署,因此可以部署在不同的云提供商的环境中。
-
高性能:Flowable具有高性能工作流建模和执行能力,可以在大量用户和大量数据的情况下快速运行。
-
灵活性:Flowable支持多种流程,并允许用户创建自定义操作,以适应各种业务需求。
-
可视化:Flowable提供了易于使用的web界面,可以轻松地设计、部署和监控工作流。
总的来说,Flowable是一个功能强大、灵活和易于使用的工作流引擎,可以帮助企业更好地管理和优化其业务流程。
2、什么是BPMN?Flowable支持哪些版本的BPMN规范?
BPMN(Business Process Model and Notation)是一种用于建模和描述业务流程的图形化标准。它提供了一组符号和规则,使得用户可以清晰地表示各种复杂的流程和活动。
Flowable支持BPMN 2.0规范,这是当前最新版本的BPMN标准。该标准包括许多新的功能和扩展,如消息、数据对象、错误处理等,以及更加灵活的事件定义和网关控制。Flowable支持BPMN 2.0中的所有元素和规则,并提供了丰富的API和工具,以帮助用户创建、执行和监视复杂的业务流程。
3、请对比一下Flowable与Activiti的异同点?
Flowable和Activiti都是用于管理、执行和监视业务流程的开源Java引擎。它们有许多相似之处,但也存在一些差异。
相同点:
- 均为基于Java的开源流程引擎。
- 两个项目的创建者和核心开发者都是同一个团队。
- 均支持BPMN 2.0规范,并提供了可视化建模工具和丰富的API以及插件。
- 都提供了与Spring等常用框架的集成。
不同点:
-
历史背景:Activiti最初是从JBoss jBPM项目中分离出来的,而Flowable则是由Activiti核心开发者创建的新项目。
-
社区活跃度:Flowable最近几年得到了较为广泛的关注和使用,社区活跃度更高;Activiti在2019年被Alfresco收购后,其发展方向和未来发展变得不确定。
-
架构设计:Flowable的架构更加灵活、模块化,可以更好地适应各种需求和场景;Activiti的架构则更加简单,适合快速实现基本的流程管理功能。
-
功能特性:Flowable在任务分配、事件处理、历史数据管理、REST API等方面拥有更多的功能特性;Activiti则更加注重用户界面的设计和易用性。
总体来说,Flowable和Activiti都是优秀的流程引擎,具有自己的优势和特点。用户可以根据实际需求和场景选择适合自己的解决方案。
4、请列举一些常用的flowable API,以及它们分别的作用?
以下是一些常用的Flowable API及其功能:
-
RepositoryService:用于管理流程定义,包括部署、删除和查询等。
-
RuntimeService:用于启动、管理和执行流程实例,包括开始、暂停、恢复和终止等。
-
TaskService:用于管理任务,包括查询、委派、转派、认领以及完成等。
-
HistoryService:用于管理历史数据,包括流程实例、任务、变量、活动和步骤等的记录和查询。
-
IdentityService:用于管理用户和组,包括创建、更新、删除和查询等。
-
ManagementService:提供了一些管理和监控流程引擎的API,如查询数据库表、修改流程参数、获取引擎信息等。
-
FormService:用于渲染任务表单,并将表单提交到后台处理。
-
ProcessEngineConfiguration:用于配置流程引擎,包括数据库连接、缓存策略、事件监听器、任务分配策略等。
通过使用这些API,用户可以在代码中轻松地集成Flowable引擎,并实现各种业务需求。
5、请说明一下flowable中流程监听器的作用,以及有哪些类型的监听器?
流程监听器在Flowable中用于监听各种事件,以便在事件发生时执行一些自定义逻辑。它们可以通过API或流程定义文件(BPMN XML)进行配置,并与各种事件关联。
Flowable中存在以下类型的监听器:
-
ExecutionListener:用于监听流程实例和执行实例的生命周期事件,如开始、结束、删除等。
-
TaskListener:用于监听任务的生命周期事件,如创建、分配、完成和删除等。
-
VariableListener:用于监听流程变量的创建、更新和删除等事件。
-
EventListener:用于监听信号、消息和定时器等事件,并触发相应的动作。
-
CaseExecutionListener:用于监听CMMN案例实例和执行实例的生命周期事件,如开始、结束、删除等。
通过使用这些监听器,用户可以在流程运行期间监视和响应各种事件,并与其他系统进行交互。例如,当某个任务被分配给一个用户时,可以触发一个TaskListener并将通知发送到用户的邮件或移动设备上。这些监听器提供了丰富的扩展性和个性化定制能力,可以满足各种需求。
6、请解释一下什么是流程实例和执行对象,在flowable中有什么关系?
在Flowable中,流程实例和执行对象是用于管理和执行业务流程的两个重要概念。
流程实例是业务流程的一个运行实例,包含了所有活动和状态信息。当一个业务流程被启动时,会创建一个新的流程实例,并在整个流程生命周期内负责管理和维护流程状态。每个流程实例都有唯一的标识符,可以用来区分不同的实例。
执行对象是业务流程的运行时对象,它代表了流程实例当前正在执行的任务或活动。例如,当一个用户任务被分配给某个用户时,就会产生一个执行对象,表示该用户任务的执行状态和相关信息。执行对象通常与流程实例密切相关,但它们并不等同于流程实例。
在Flowable中,流程实例和执行对象之间存在一种父子关系。每个执行对象都有一个父级执行对象,代表其所属的流程实例。通过这种方式,Flowable能够有效地管理和执行复杂的流程实例,同时提供丰富的API和工具来查询、更新和监视流程实例和执行对象的状态。
7、请说明一下flowable中流程定义的基本组成部分,以及这些组成部分的作用。
在Flowable中,流程定义是用于描述和管理业务流程的一个重要概念。它由以下几个基本组成部分构成:
-
流程图:流程图是用于可视化表示流程执行顺序和流转方向的一种图形化表示方式,使用BPMN 2.0标准符号进行建模。在流程定义中,流程图用于描述整个业务流程的结构和逻辑。
-
节点:节点是流程图中的基本元素,代表了流程中的各种任务、活动、网关等。每个节点都有自己的名称、类型、输入输出参数等属性,用于指定节点的行为和功能。
-
连线:连线用于连接节点之间的关系,并表示流程执行的先后顺序。它们可以是有向边或无向边,可以包含条件和事件等约束条件。
-
变量:变量用于存储和传递流程中的数据和状态信息。在流程定义中,可以定义各种变量和变量类型,并在不同的节点中进行读写操作。
通过使用这些组成部分,用户可以轻松地创建、修改和管理业务流程,并通过Flowable引擎实现流程的管理和执行。流程定义提供了一个通用的框架,以支持各种复杂的工作流和业务流程需求。
8、如何使用Flowable创建和部署一个流程定义?
使用Flowable创建和部署一个流程定义通常需要以下步骤:
-
创建BPMN 2.0流程图:使用BPMN 2.0规范的符号和元素,设计和绘制业务流程的流程图。可以使用Flowable Modeler等工具进行可视化建模。
-
将BPMN 2.0流程图转换为XML格式:在完成流程图的设计后,将其导出为BPMN 2.0 XML文件,并保存到本地或者版本控制系统中。
-
配置流程引擎:使用Flowable提供的API或配置文件,配置流程引擎的参数和属性,如数据库连接、缓存策略、事件监听器、任务分配策略等。
-
部署流程定义:使用RepositoryService API向流程引擎上传BPMN 2.0 XML文件,将其编译成可执行的流程定义,并进行部署。部署操作通常包括以下几个步骤:创建DeploymentBuilder实例、添加BPMN 2.0 XML文件、设置部署名称和类别等属性、执行部署操作。
-
启动流程实例:使用RuntimeService API启动流程实例,创建一个新的流程执行对象,开始执行业务流程。可以通过传递变量、指定流程启动人和其他参数来控制流程的行为和执行顺序。
以上步骤只是一个大致的流程,并不是详尽的描述。具体实现细节和操作方式可以参考Flowable官方文档和示例代码。
9、如何启动、暂停、删除、挂起某个具体的流程实例?
在Flowable中,可以使用RuntimeService API启动、暂停、删除和挂起某个具体的流程实例。以下是每个操作对应的API方法:
-
启动流程实例:使用startProcessInstanceByKey()或startProcessInstanceById()方法启动流程实例。其中,startProcessInstanceByKey()方法根据流程定义的key启动流程实例,而startProcessInstanceById()方法根据流程定义的ID启动流程实例。
-
暂停流程实例:使用suspendProcessInstanceById()方法暂停流程实例。该方法接受一个流程实例ID参数,并将其暂停。在暂停状态下,流程实例不会执行任何活动或任务。可以使用activateProcessInstanceById()方法恢复流程实例的正常执行。
-
删除流程实例:使用deleteProcessInstance()或deleteProcessInstances()方法删除流程实例。其中,deleteProcessInstance()方法根据流程实例ID删除指定的流程实例,而deleteProcessInstances()方法通过过滤器批量删除符合条件的流程实例。删除流程实例时,还可以选择是否级联删除相关的历史数据和运行时数据。
-
挂起流程实例:使用suspendProcessInstanceById()方法挂起流程实例。该方法与暂停流程实例类似,但挂起状态下,流程实例不能被恢复,除非先将其激活。
需要注意的是,在暂停或挂起流程实例时,需要考虑相关的任务、事件和变量等的状态和处理方式,以免影响流程实例的正确性和一致性。同时,也需要谨慎处理删除操作,以避免误删重要的数据和记录。
10、在Flowable中,如何查询并处理当前待办的任务?
在Flowable中,可以使用TaskService API查询和处理当前待办的任务。以下是一些常用的API方法:
1、查询待办任务列表:
使用createTaskQuery()
方法创建一个TaskQuery对象,然后设置各种过滤条件(如任务的候选人、办理人、所属流程实例等),最后调用list()
或singleResult()
方法获取符合条件的待办任务列表。例如,以下代码查询当前用户的所有待办任务:
List<Task> tasks = taskService.createTaskQuery() .taskCandidateOrAssigned("userId") .list();
获取待办任务详情:通过TaskService API提供的getTaskById()方法,根据任务ID获取指定的待办任务,并可以获取相关属性,如任务名称、所属流程定义、候选人/受理人等。
完成待办任务:使用complete()或claim()方法完成待办任务。其中,complete()方法用于标记任务已经完成,并将执行结果传递给下一个节点;而claim()方法用于将任务分配给某个用户或者组,以便其进行处理。例如,以下代码完成某个任务并将执行结果传递给下一个节点:
taskService.complete(taskId, variables);
转派待办任务:使用setAssignee()
方法将待办任务转派给其他用户或组。例如,以下代码将某个任务转派给其他用户:
taskService.setAssignee(taskId, "newAssigneeId");
除了以上方法,TaskService API还提供了各种其他的查询和处理API,如委托、转办、查询历史任务等。用户可以根据实际需求,选择适合自己的API方法进行操作。
11、如何使用Flowable实现子流程的调用和管理?
在Flowable中,可以使用子流程来实现业务流程的模块化和复用。以下是使用Flowable实现子流程调用和管理的一些基本步骤:
-
创建子流程:使用BPMN 2.0规范的符号和元素,设计和绘制子流程的流程图。可以使用Flowable Modeler等工具进行可视化建模,并将其保存为独立的BPMN 2.0 XML文件。
-
部署子流程:通过RepositoryService API向流程引擎上传BPMN 2.0 XML文件,将其编译成可执行的子流程定义,并进行部署。可以使用以下代码实现子流程的部署:
Deployment deployment = repositoryService.createDeployment() .addClasspathResource("subprocess.bpmn20.xml") .deploy();
调用子流程:在主流程中,通过BPMN 2.0规范的Call Activity元素,调用已经部署的子流程。Call Activity元素有两种类型:independent(独立)和embedded(嵌入)。其中,独立类型的子流程会创建一个新的独立流程实例,而嵌入类型的子流程会在当前流程实例内部直接执行。例如,以下代码在主流程中调用独立的子流程:
ProcessInstance subProcessInstance = runtimeService .createProcessInstanceByKey("subProcess") .businessKey(businessKey) .execute();
管理子流程:在子流程中,可以使用Flowable提供的各种API方法,管理和控制子流程的执行。例如,可以使用ExecutionService API查询当前正在运行的子流程实例、设置子流程变量等