基线组件学习
基线组件学习记录,看到看到哪写到哪
Redis
Redis分布式锁
Redis分布式锁介绍 - Platform&Framework - ZAI Confluence (zatech.com)
实现方式:
SETNX KEY VALUE+EXPIRE
- 加锁: SETNX key value.当键不存在时,对键进行设置操作并返回成功,否则返回失败.key是锁的唯一标识
- 解锁: DEL key, 直接删除
- 锁超时: EXPIRE key timeout,设置key的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源永远被锁住.
SET KEY VALUE EX|PX NX|XX
set命令支持设置更多可选参数
问题:
- SETNX和设置超时时间是非原子操作.可能会死锁
解决:
使用set key value time NX命令替代
-
锁误解除
如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。
解决: 充分利用加锁时的value,生成UUID或线程号放入value.释放锁时判断是不是自己的锁;
-
超时解锁导致并发
锁误解除的时候,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。
解决方案:
1. 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成. 2. 为获取锁的线程增加守护线程,为将要过期但未释放的锁增 加有效时间(看门狗?)
-
不可重入
当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。
解决方案:
使用 ThreadLocal 进行重入次数统计
Redis Map 数据结构来实现分布式锁,既存锁的标识也对重入次数进行计数
- 集群下主备切换
为了保证 Redis 的可用性,一般采用主从方式部署。主从数据同步有异步和同步两种方式,Redis 将指令记录在本地内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一致的状态,一边向主节点反馈同 步情况。
在包含主从模式的集群部署方式中,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,指令还 未同步,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功。
-
集群脑裂
集群脑裂指因为网络问题,导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区,因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为 master 节点,此时存在两个不同的 master 节点。Redis Cluster 集群部署方式同理。
当不同的客户端连接不同的 master 节点时,两个客户端可以同时拥有同一把锁
Spring官方实现(Spring Integration)
四个存储实现
- Gemfire
- JDBC
- Redis
- Zookeeper
使用相同的API抽象.不论使用哪种存储,你的编码体验是一样的.有一天想更换实现,只需要修改依赖和配置就可以了.无需修改代码
@Autowired
private RedisLockRegistry redisLockRegistry;
public void test() throws InterruptedException {
Lock lock = redisLockRegistry.obtain("lock");
boolean b1 = lock.tryLock(3, TimeUnit.SECONDS);
log.info("b1 is : {}", b1);
TimeUnit.SECONDS.sleep(5);
boolean b2 = lock.tryLock(3, TimeUnit.SECONDS);
log.info("b2 is : {}", b2);
lock.unlock();
lock.unlock();
}
Kafka
Kafka相关资料
Kafka相关资料 - Platform&Framework - ZAI Confluence (zatech.com)
- 整体流程图:
-
流程描述
2.1 主线程中完成
2.1.1 创建KafkaProducer:
1) 需要指定broker的地址:
并非需要所有的broker地址,生产者会从给定的broker里查找到其他broker的信息.不过建议至少要设置两个以上的broker地址信息,当其中任意一个宕机时,生产者仍然可以连接到kafka集群上.
2) 指定key, value序列化方式: 必须指定,无默认值
2.1.2 创建消息:
new ProducerRecord<String, String>(
topic, //消息主题。
null, //分区编号。建议为null,由Producer分配。
System.currentTimeMillis(), //时间戳。
String.valueOf(value.hashCode()), //消息键。
value //消息值。
);
2.1.3. 生产者拦截器: 不是必须的
生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息,修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求.比如统计类工作.自定义生产者拦截器需要实现org.apache.kafka.clients.producer.ProducerInterceptor接口,重写onSend方法(发送之前会被调用),onAcknowledgement方法(消息被应答之前或消息发送失败时调用)
2.1.4. 序列化器: 必须
将消息序列化便于传输到broker
2.1.5. 分区器
分区器的作用就是为消息分配分区, 如果消息ProducerRecord中指定了partition字段, 那么就不需要分区器的作用, 因为partition代表的就是所要发往的分区号. 如果消息ProducerRecord中没有指定partition字段, 那么就需要依赖分区器, 根据key这个字段来计算partition的值
默认的分区器:
如果key不为null, 那么默认的分区器会对key进行哈希(采用MurmurHash2算法,高性能低碰撞), 最终根据得到的哈希值来计算分区号, 拥有相同key的消息会被写入同一个分区. 如果key为null,那么消息会以轮询的方式发往主题内的各个可用分区
可参考如下代码实现粘性分区策略,该代码的实现逻辑主要是根据一定的时间间隔,切换一次分区.
public class MyStickyPartitioner implements Partitioner {
// 记录上一次切换分区时间。
private long lastPartitionChangeTimeMillis = 0L;
// 记录当前分区。
private int currentPartition = -1;
// 分区切换时间间隔,可以根据实际业务选择切换分区的时间间隔。
private long partitionChangeTimeGap = 100L;
public void configure(Map<String, ?> configs) {}
/**
* Compute the partition for the given record.
*
* @param topic The topic name
* @param key The key to partition on (or null if no key)
* @param keyBytes serialized key to partition on (or null if no key)
* @param value The value to partition on or null
* @param valueBytes serialized value to partition on or null
* @param cluster The current cluster metadata
*/
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 获取所有分区信息。
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
int availablePartitionSize = availablePartitions.size();
// 判断当前可用分区。
if (availablePartitionSize > 0) {
handlePartitionChange(availablePartitionSize);
return availablePartitions.get(currentPartition).partition();
} else {
handlePartitionChange(numPartitions);
return currentPartition;
}
} else {
// 对于有key的消息,根据key的哈希值选择分区。
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
private void handlePartitionChange(int partitionNum) {
long currentTimeMillis = System.currentTimeMillis();
// 如果超过分区切换时间间隔,则切换下一个分区,否则还是选择之前的分区。
if (currentTimeMillis - lastPartitionChangeTimeMillis >= partitionChangeTimeGap
|| currentPartition < 0 || currentPartition >= partitionNum) {
lastPartitionChangeTimeMillis = currentTimeMillis;
currentPartition = Utils.toPositive(ThreadLocalRandom.current().nextInt()) % partitionNum;
}
}
public void close() {}
}
2.2.sender线程中完成:
从RecordAccumulator中获取消息并将其发送到Kafka中.
2.3.RecordAccumulator介绍(批量发送器):
1) 作用: 缓存消息以便Sender线程可以批量发送,进而减少网络传输的资源消耗以提升性能.
2) 大小设置: RecordAccumulator缓存的大小可以通过生产者客户端参数buffer,memory配置,默认值为33554432B,即32MB
3) 注意:
如果生产者发送消息的速度超过发送到服务器的速度,会导致生产者空间不足,这个时候KafkaProducer的send()方法调用要么被阻塞,要么抛出异常,这个取决于参数max.block.ms的配置.此参数的默认值为60000,即60秒.
很好理解,生产者不足
4) 工作原理:
在RecordAccumulator的内部为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,即Deque.消息写入缓存时,追加到双端队列的尾部;Sender读取消息时,从双端队列的头部读取.注意ProducerBatch不是ProducerRecord. RecordAccumulator的内部还有一个BufferPool,它主要用来实现ByteBuffer的复用(直接内存),以实现缓存的高效利用.不过BufferPool只针对特定大小的ByteBuffer进行管理,而其他大小的ByteBuffer不会缓存进BufferPool中.这个特定的大小由batch.size参数来指定,默认值为16KB.我们可以适当地调大batch.size参数以便多缓存一些消息.
5) 消息进入RecordAccumulator 过程:
一条消息(ProducerRecord)流入RecordAccumulator时,会先寻找与消息分区所对应的双端队列(如果没有则新建).查看ProducerBatch中是否还可以写入这个ProducerRecord.如果可以则写入,如果不可以则需要创建一个新的ProducerBatch.在新建ProducerBatch时评估这条消息的大小是否超过batch.size参数的大小.如果超过,那么就以评估的大小来创建ProducerBatch,这段内存区域不会被复用.
6) InFlightRequests作用:
请求在Sender线程发往Kafka之前还会保存到InFlightRequests中,Map<NodeId,Deque>,它的主要作用是缓存了已经发出去但还没有收到响应的请求(NodeId是一个String类型,表示节点的id编号).max.in.flight.requests.per.connection.默认为5,即每个连接最多只能缓存5个未响应的请求.超过该数值之后就不能再向这个链接发送更多的请求了,除非有缓存的请求收到了响应(Response),通过比较Deque的size与这个参数的大小来判断对应的Node中是否已经堆积了很多未响应的消息,如果真是如此,那么说明这个Node节点的负责较大或网络连接有问题,再继续向其发送请求会增大请求超时的可能
重要的生产者参数
-
Acks:
acks=0: 无需服务端的Response,性能高,丢数据风险大
acks=1: 服务端主节点写成功即返回Response,性能中等,丢数据风险中等,主节点宕机可能导致数据丢失
acks=all: 服务端主节点写成功且备节点同步成功才返回Response,性能较差,数据较为安全.主节点和备节点都宕机才会导致数据丢失.
一般建议选择acks=1,重要的服务可以设置acks=all.即使设置成all,也会有丢数据的风险.比如当ISR中只有leader副本,要获得更高的消息可靠性需要配合min.insync.replicas等参数的联动
-
max.request.size:
这个参数用来限制生产者客户端能发送的消息的最大值,默认值为1048576b,即1MB.一般情况下,这个默认值可以满足大多数情景
-
retries:
参数用来配置生产者重试的次数,默认值为0,即在发生异常的时候不进行任何重试的动作.消息在从生产者发出到成功写入服务器之前可能发生一些临时性的异常.比如网络抖动,leader副本选举等.这种异常往往是可以自行回复的,生产者可以通过配置retries大于0的值,以此通过内部重试来回复而不是一味将异常抛给生产者的应用程序.
-
retry.back.msretries:
这个参数默认值为100,用来设定两次重试之间的时间间隔,便民无效的频繁重试.在配置retries和retry.backoff.ms之前,最好先估算一下可能的异常回复时间,以此来避免生产者过早地放弃重试
-
batch.size:
发往每个分区(Partition)的消息缓存量(消息内容的字节数之和,不是条数).达到设置的数值时,就会触发一次网络请求,然后Producer客户端把消息批量发往服务器,可提升发送性能
-
connections.max.idle.ms:
这个参数用来指定在多久之后关闭限制的连接,默认值是540000(ms),即9分钟
-
linger.ms:
这个参数用来指定生产者发送ProducerBatch之前等待更多的消息(ProducerRecord)加入ProducerBatch的时间,默认值为0.生产者客户端会在ProducerBatch被填满或者等待时间超过linger.ms值时发送出去.增大这个参数的值会增强消息的延迟,但是同时能提升一定的吞吐量.这个linger.ms参数与TCP协议中的Nagle算法有异曲同工之妙
-
receive.buffer.bytes:
这个参数用来设置Socket发送消息缓冲区的大小,默认值为131072,即128KB.与receive.buffer.bytes参数一样,如果设置为-1.则使用操作系统的默认值.
-
sender.buffer.bytes:
这个参数用来设置Socket发送消息缓冲区的大小.默认值为131072,即128KB.与receive.buffer.bytes参数一样,如果设置为-1,则使用操作系统的默认值.
-
request.timeout.ms
这个参数用来配置Producer等待请求响应的最长时间,默认值为30000(ms)。请求超时之后可以选择进行重试。注意这个参数需要比broker端参数replica.lag.time.max.ms的值要大,这样可以减少因客户端重试而引起的消息重复的概率。
-
max.in.flight.requests.per.connection
已发送但是还没有得到响应的最大消息数,一般而言,在需要保证消息顺序的场合建议把参数配置为1,不过这样也会影响整体的吞吐。
消费者消费消息
- 消费者和消费组
对于一个消费组,一个Topic的分区会让不同的消费者来负责消费,此外一个消费组特可以订阅多个不同的Topic,建议一个消费组订阅的 Topic不要超过5个.多个消费组可以订阅相同的Topic
-
消费者消息消息:
public ConsumerRecords<K, V> poll(Duration duration); 参数意思是,如果没有拉取到消息,最多阻塞指定的时间就返回了。
-
位移提交:
-
什么是位移提交:
消费者需要标记自己消费消息的位置,将下次要拉去的消息位置持久化的过程就是位移提交.如果不保存位移,那消费者异常重启可能会重复消费消息
-
自动提交:
enable.auto.commit设置成true,每隔auto.commit.intervals.ms(默认5s)设置值时间,会提交位移.
自动提交有可能导致重复消费,设想在下次提交之前,消费者异常了,重启后会消费上次已经提交的消息
-
手动提交:
-
同步提交:
consumer.commitSync(),程序会处于阻塞状态,直到远端的Broker返回提交结果,这个状态才会结束.
-
异步提交:
consumer.commitAsync(),有重载方法,commitAsync是否能够替代commitSync呢?答案是不能.commitAsync的问题在于,出现问题时不会自动重试.因为它是异步操作,倘若提交失败后自动重试,那么它重试时提交的位移值可能早已经"过期"或不是最新值了.因此,异步提交的重试其实没有意义.所以commitAsync不会重试—合理
-
同步异步结合
try{ while(true){ ConsumerRecords<String,String> records = consumer.poll(Duration.OfSeconds(1)); process(records); // 处理消息 comsumer.commitAsync(); // 使用异步提交规避阻塞 } }catch(Exception e){ handle(e); // 处理异常 }finally{ try{ consumer.commitSync(); // 最后一次提交使用同步阻塞提交 }finally{ consumer.close(); } }
-
指定位移消费:
seek();
-
-
Rebalance:
-
影响:
-
在rebalance发生期间,消费组内的消费者是无法读取消息的.也就是说,在rebalance发生期间的这一小段时间内,消费组会变得不可用.
-
当一个分区被重新分配给另一个消费者时,消费者当前的状态也会丢失.比如消费者消费完某个分区中的一部分消息时还没有来得及提交消费位移就发生了rebalance,之后这个分区又被分配给了消费组内的另一个消费者.原来被消费完的那部分消息又被重新消费一遍,也就是发生了重复消费.一般情况下,应尽量避免不必要的rebalance
-
触发条件
- 组成员数发生变更.比如有新的consumer实例加入组或者离开组,又或是有Consumer实例崩溃被"踢出"组(99%原因)
- 订阅topic数发生变更.Consumer Group可以使用正则表达式的方式订阅topic.比如consumer.subscribe(Pattern.compile(“t.*c”))就表明该Group订阅所有以字母t开头,字母c结尾的topic.在Consumer Group的运行过程中,你新创建了一个满足这样条件的topic,那么该topic就会发生Rebalance
- 订阅topic的分区数发生变化,Kafka目前只允许增加topic分区数.当分区数增加时,就会出发订阅该topic的所有group开启Rebalance
-
如何避免:
见消费者问题小节
-
-
-
重要的消费者参数:
-
fetch.min.bytes:
该参数用来配置Consumer在一次拉取请求(一次poll()方法)中能从Kafka中拉取的最小数据量.默认值为1.Kafka在收到Consumer的拉取请求时,如果返回给Consumer的数据量小于这个参数所配置的值,那么它就需要进行等待.直到数据量满足这个参数的配置大小.
可以适当调大这个参数的值以提高一定的吞吐量.不过也会造成额外的延迟(latency),对于延迟敏感的应用可能就不可取了.
-
fetch.max.bytes:
该参数与fetch.min.bytes参数对应,它用来配置Consumer在一次拉取中最大数据量.默认50MB.如果这条参数设置的值比任何一条写入Kafka中的消息要小,那么会不会造成无法消费呢?
很多资料对此参数的解读认为是无法消费的.比如一条消息的大小为10B,而这个参数的值是1.既然此参数设定的值是一次拉取请求中所能拉取的最大数据量,那么显然1B<10B,所以无法拉取观点错误该参数设定的不是绝对的最大是,如果在第一个非空分区中拉取的第一条消息大于该值,那么该消息将仍然返回,以确保消费者继续工作.也就是说,上面问题的答案是可以正常消费.与此相关的,Kafka中所能接收的最大消息通过服务端参数message.max.bytes(对应于主题端参数max.message.bytes)来设置.
-
fetch.max.wait.ms:
这个参数也和fetch.min.bytes参数有关.如果Kafka仅仅参考fetch.min.bytes参数的要求,那么有可能会一直阻塞等待而无法发送响应给Consumer,显然不合理.
fetch.max.wait.ms参数用于指定Kafka的等待时间,默认值为500(ms).如果Kafka中没有足够多的消息而满足不了fetch.min.bytes参数的要求,那么最终会等待500ms.这个参数的设定和Consumer与Kafka之间的延迟也有关系,如果业务应用对延迟敏感,那么可以适当调小这个参数.
-
max.partition.fetch.bytes:
这个参数用来配置从每个分区里返回给Consumer的最大数据量,默认值为1048576(B),即1MB.这个参数与fetch.max.bytes参数相似.只不过前者用来限制一次拉取中每个分区的消息大小,而后者用来限制一次拉取中整体消息大小.同样.如果这个参数设定的值比消息的大小要小,那么也不会造成无法消费.Kafka为了保持消费逻辑的正常运转不会对此做强硬限制.
-
max.poll.records:
这个参数用来配置Consumer在一次拉取请求中拉取的最大消息数,默认值为500条.如果消息的大小都比较小,则可以适当调大这个参数值来提升一定的消费速度
-
connection.max.idle.ms:
这个参数用来指定在多久之后关闭限制的连接,默认是540000ms,即9分钟
-
exclude.internal.topics:
Kafka中有两个内部的主题:
- consumer_offsets
- tansaction_state
exclude.interval.topics用来指定Kafka中的内部主题是否可以向消费者公开.默认为true.
如果设置为true,那么只能使用subscribe(collection)的方式而不能使用subscribe(Pattern)的方式来订阅内部主题,设置为false则没有这个限制.
-
receive.buffer.bytes:
这个参数用来设置Socket接收消息缓冲区的大小,默认值为65536B 64KB.如果设置为-1,则使用操作系统的默认值.如果Consumer与Kafka处于不同的机房,则可以适当调大这个参数值.
-
send.buffer.bytes:
这个参数用来设置Socket接收消息缓冲区(SO_SNDBUF)的大小,默认值为131072B,即128KB.与receive.buffer.bytes参数一样,如果设置为-1,则使用操作系统的默认值
-
request.timeout.ms
配置COnsumer等待请求响应的最长时间,默认30000(ms)
-
metadata.max.age.ms:
这个参数用来配置元数据的过期时间,默认值为300000(ms),五分钟
-
reconnect.backoff.ms:
这个参数用来配置尝试重新连接指定主机之前的等待时间(也称为退避时间).避免频繁连接主机,默认值为50ms.这种机制适用于消费者向broker发送的所有请求
-
retry.backoff.ms:
这个参数用来配置尝试重新发送失败请求到指定的主题分区之前的等待(退避)时间,避免在某些故障情况下频繁地重复发送,默认值为100
Kafka相关问题:
生产者
-
生产者会建立多少个到Broker的连接
每个生产者通常会建立2个到Broker的TCP连接,一个TCP连接用于更新元数据,一个TCP连接用于发送消息.
相关链接: https://stackoverflow.com/questions/47936073/how-are-tcp-connections-managed-by-kafka-clients-scala-library?spm=a2c4g.11186623.0.0.6b6628c21Q39Tu&rq=1
-
Java客户端设置回调是否会影响消息发送的速度
消费者
Kafka消息乱序
- 问题背景
Fusion理赔同步发现,同一笔理赔的状态跟Graphene中不一致,经日志查询,几乎同一时间点同一个报案号同步了两笔数据,并且状态不一样,最终落库时发现最先过来的那一笔成为了最终状态,这违背了我们的期望值。
很单纯的消息分区问题导致消费乱序,没有什么好说的
- 问题分析
通过kafka的原理简单分析得知,消息生产的时候是轮询分配到各个分区,多个消费者从分区进行消费,由于消息顺序不可控,导致最终乱序的问题。根据kafka对消息分区官方解释:
- 如果发消息时候指定分区,则消息投递到指定的分区
- 如果没有指定分区,但是消息的key不为空,则基于key的哈希值来选择分区
- 如果既没有指定分区,且消息的key也是空,则用轮询的方式来选择一个分区
明白了消息分区,要解决这个问题是非常简单了.只要放在一个分区即可.
动态字段
customer动态字段
Customer Element Configuration
对应存储数据库表:
数据库: metadata_00
系统字段: schema_def_field
动态字段:
- schema_def_field_extension : 动态字段定义表
schema_def_type=1为customer相关字段, 参考枚举:SchemaDefTypeEnum
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rdQPzMHL-1646807612638)(基线组件学习.assets/image-20220303153928755.png)]
- schema_def_field_property : 动态字段配置表 (根据code字段关联schema_def_field_extension)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OG0IvaPZ-1646807612639)(基线组件学习.assets/image-20220303153957199.png)]
标的动态字段(vnaia暂不涉及)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Llr1W2f-1646807612640)(基线组件学习.assets/image-20220303153642837.png)]
对应存储数据库表:
数据库: product_00
系统字段: schema_def_field(metadata_00)
动态字段: product_schema_extension_field_def
常见问题应对
幂等
- 利用redis分布式锁+数据库历史查询
首先利用redis分布式锁,保证短时间内没有重复处理
再到历史库查询以往也没有重复处理.
优点: 对历史表结构及数据没有特殊要求,判断比较灵活
缺点: 依赖组件多,略微损失性能
代码: if…else…
-
利用数据库唯一索引
拿到数据后首先进行存库,如果保存时违反约束,则表示消息重复
优点: 不依赖多个组件实现
缺点: 需要先存库再更新,有性能损失.如果一个请求对应多次返回,需要讲返回数据做一对多表
代码性热爱为try…catch
设计(按方案一)
因有可能一次请求多次返回,所以将返回单独做一张表
sqs_history(主)
字段 | 备注 |
---|---|
request_id | 请求唯一标识 |
request_type | 请求类别 |
request_data | 请求入参 |
total | 保单数量 |
remark | 备注 |
gmt_created | 创建时间 |
sqs_response(从)
字段 | 备注 |
---|---|
request_id | 同主表 |
request_type | 同主表 |
response_data | 每次返回数据 |
success | 本次成功 |
fail | 本次失败 |
remark | 备注 |
gmt_created | 创建时间 |
有关表数据的处理均放在EventHandler里
处理步骤:
1、当JSON解析失败,记history,request_id=UUID,total=-1,KLOOK重发
2、当参数校验失败,记history,request_id=UUID,total=-1,KLOOK重发
3、取request_id=channelOrderId,request_type=TRADE_CREATE,计算total
4、先到redis中校验近30分钟是否有重复request_id和request_type,如果有则消息重复;再到history表查询request_id和request_type,如果有则消息重复;重复消息仍然记录一条history,request_id=UUID,total=-1
5、对SQSBiz调用try…catch,如有异常,记录一条history和一条response,fail=total(有风险,不能确定全部失败),response_data=null
6、SQSBiz依次出单,统计出单结果并返回给EventHandler,EventHandler记录一条history和一条response
综上,任何消息均会被记录一条主表,当主表total=-1,则该条数据为丢弃数据,如果需要补则由KLOOK修改数据后再重新推;
否则一般由ZA来补,查询response表可以获取某一次请求的成功和失败数量(异步的业务可能有多条response),可通过remark查询具体是哪一个保单出错,确认后进行补单。
history和response表保存可通过EventPublisher推送给异步线程进行保存。