项目整体分为后管模块、分发模块、引擎模块、结算模块。
后管模块:面向商家,负责优惠券的创建、以及目标用户的优惠券分发任务
分发模块:面向商家,负责执行百万级别优惠券的分发任务
引擎模块:面向用户,为用户提供优惠券查询、兑换、秒杀以及秒杀活动预约等功能,并在在用户创建订单时进行优惠券的锁定、核销与退还。
结算模块:面向用户,为用户提供结算时可用与不可用优惠券的查询功能,并且计算订单的结算金额。
一、后管模块
问:优惠券创建and订单创建如何做幂等
优惠券库表设计:
分布式主键ID:雪花算法,预留位数防止冲突
优惠券本身的参数:优惠券ID、所属店铺编号、优惠券来源(店铺、平台)、优惠对象(商品专属、全店通用)、优惠类型(满减、折扣)、库存、有效时间、截至时间、消费规则、领取规则、优惠券状态。
责任链模式创建优惠券:
责任链可以将客户端的请求与多个处理方进行解耦,使用的时候只需要知道责任链的入口,请求就会在多个处理器之间传递。
责任链模式使用到了责任链模式、简单工厂、策略模式。我们抽象出责任链处理的节点、责任链组装的逻辑,调用默认工厂模式去创建责任链。具体来说,责任链包含三个节点:校验空值、校验参数是否合法(商品专属优惠需要设置指定的对象,全店通用不能设置优惠对象,库存是否违规)、校验参数是否真实(调商品中台校验商品是否存在)。
这里主要指,防止因网络延时、系统处理延时导致用户重复提交、用户前端重复点击导致短时间内反复创建。
前端:前端点击后禁用按钮
后端幂等:
1、利用token机制:对于订单id则生成唯一token,对于优惠券则用券ID+请求参数MD5码生成token。请求来的时候先检查token是否被使用,如果被使用就直接返回。
2、基于数据库的唯一索引:对多个字段创建唯一的联合索引防重。
3、分布式锁:使用redisson分布式锁保证同一时间只允许处理一个创建请求。
4、简单地用布隆过滤器。
5、滑动窗口限流,限制一段时间内只能点击依次。
创建优惠券时基于AOP+redisson实现创建订单前根据优惠券ID+参数MD5码加分布式锁,确保短时间内只能创建一张相同的优惠券。数据库层面创建唯一索引防重。
具体来说:
而商家隔一段时间创建重复的优惠券可以通过数据库唯一索引校验。
使用Redisson+SpringAOP实现幂等注解,防止商户重复创建优惠券
描述:定义Spring AOP切面类,使用环绕通知对创建优惠券方法进行增强,基于注解来描述切面类的切入点。
环绕通知里在Controller层执行创建优惠券任务前会尝试获得Redisson分布式锁,只有成功拿到分布式锁才执行创建优惠券逻辑,然后释放锁,返回方法执行的结果,也就是创建优惠券。
分布式锁的key:业务前缀+请求路径+商户ID+方法参数的MD5
问:数据量有多大?如何分库分表?
对于高并发写入与海量数据场景下就需要分库分表。当表数据过多,不仅读取性能会变慢,写入数据的时候也会出现行锁竞争,影响性能。高并发写入下,单库的连接数有限,扛不住,就需要分库。
对于天猫和淘宝非官方统计3000万个用户,折中假设一个商家创建100张优惠券,30亿张优惠券。
对于分库:分库取决于业务场景的TPS数,我之前看过腾讯云的MYSQL压测报告,16核128GB的MYSQL单机写操作是5000~6000TPS,假定业务场景是一万TPS,那么至少分两个库。
对于分表:分表取决于实际业务数据量,根据非官方统计,淘宝和天猫的商户大概在3000万,折中每个商家创建100张优惠券,那就有30亿条数据。每张表能存的数据和字段值有关,有的经验值说是2000万,阿里的技术手册里是单表不超过500万条,单表文件体积不超过2GB。我这里取500万行数据,也就需要分60张表。
我这里对于优惠券表的一行数据估算占用内存大约为150个字节。三层B+树:前两层主键索引+指针占用14个字节,16384/14=1170,最后一层16384/150=110,所以一张表最多能存的数据是一亿五千条数据。
但是根据阿里技术手册为了性能更高,取一张表500万条数据,不超过2GB.
分片键:选择商家ID,因为这样也便于商户查询创建优惠券,不需要跨库查询!
路由算法:分片算法一般有hash取模、时间范围分片、一致hash。hash取模会存在明显的数据倾斜,而随着入驻商家越来越多,后期的数据肯定是更多的,也不适合按照时间范围分片。自定义了一个分片算法解决hash取模的数据倾斜问题:int mod = (int) hashShardingValue(id) % shardingCount / (shardingCount / dbSize);
取模的结果再除以每一个数据库的分片数。
为什么不使用分布式数据库?
分布式数据库有阿里云、腾讯云等。分布式数据库一般适合小场景做测试用,不使用它的原因首先是兼容性与技术迁移成本:一些分布式数据库不能完全兼容MYSQL,业务无法平滑迁移。其次是需要一定的技术储备:需要分布式数据库专家,产品上线之后出现了问题得及时解决。使用成本:阿里云和腾讯云不开源,成本也较高。分布式数据库目前没有分库分表稳定,github里面TiDB开源版本Issue很多,暂时没得到解决。
使用SharedingSphere对优惠券、用户领券记录等进行分库分表,提高系统的并发性能与数据查询性能。使 用EasyExcel解析百万量级用户优惠券推送Excel文件,解决Excel解析地OOM问题。
描述:
为什么使用EasyExcel读取数据:
使用Faker生成100万行数据,其中包括:姓名、手机号、邮箱。内存占用30MB.
使用Hutool解析100万行数据,30MB,使用visualvm查看堆内存占用3GB。原因是他会把Excel数据全部加载到堆内存里面,很容易导致OOM。
Apache POI的XSSFWorkbook和SXSSFWorkbook在读取的时候也是如此。使用POI解析Excel操作复杂,有大量的代码冗余,容易发生OOM
EasyExcel解析堆内存最高时250MB左右。因为EasyExcel读取文件可以一行行读取与解析、每一次读取数据之后可以对其进行处理,也有读取完所有数据之后再处理的重写方法。分批次读取与解析,这样的话处理完的数据后面可以被GC,不会一直占用堆内存。
EastExcel的底层逻辑是逐行读取,用户只需要重写解析一行数据所触发的监听器的逻辑,也就是解析一行之后就rowCount++,最后得到总行数。
一张表最多存多少数据合适?
一张表性能瓶颈和MYSQL的内存大小相关,因为索引会先装载到内存里性能才会高,如果表数据到达一个上限会导致内存装不下索引,之后的SQL查询有磁盘IO,这才会导致性能下降。
实际上,我项目里是遵循阿里开发手册,一张表不超过500万或者2GB的原则。
问:头部店铺的优惠券数量会远大于普通店铺优惠券数量,会造成数据倾斜的问题吗?
就算头部店铺创建的优惠券高达10万,一张其实也是可以存储的;
如果确实存在了数据倾斜比较严重,则进行冷热分离,把一些历史数据移入冷库。
问:创建优惠券分发任务(线程池+Redis延时队列兜底 )
使用线程池异步解析Excel行数、创建优惠券分发任务,因为每次同步解析一百万行Excel需要5秒钟,加上参数验证的时间可能更长,因此为了提升接口的响应时间,我先将创建优惠券分发任务表先存储并响应,然后后台用线程池异步去更新Excel行数,也就是优惠券的分发数(一个线程负责创建一个优惠券分发任务之后直接返回,线程池负责Excel解析文件的行数)。
在线程池执行解析任务之前,提交任务到延时队列进行兜底,延时队列的key:业务key前缀+Excel地址+分发任务ID,延时队列20s之后到数据库查询优惠券分发任务是否有用户个数信息。
问:为什么使用消息队列立即执行分发优惠券任务,但是使用XXL-Job执行定时分发优惠券任务?
消息队列异步做解耦,提升吞吐量
XXJob是一种轻量化的分布式任务调度中心,我认为他相比于消息队列的优势有如下几点:
(1)提供统一灵活的任务管理的界面,可以动态的添加、修改和删除任务。
(2)支持任意秒级调度,有多种调度策略:固定频率、固定延迟等
(3)支持分片任务,非常适用于处理大数据量的任务,可以利用集群的能力同时调度多个及其运行任务。在路由策略里选择分片广播即可。
(4)部署简单,兼容性好,服务节点只需要向XXL注册执行器(节点地址)即可。
分片任务的实现原理:任务分配、分片参数、分片处理、结果汇总,一般来说就是配置分片索引与分片总数即可。
二、优惠券分发任务的流程?
答:分发流程为了减少redis和DB的IO次数进行批量操作,大体分为两个阶段。
第一阶段:线程池异步解析Excel数据,进行redis库存预扣减,保存用户信息,达到批量保存阈值后发送MQ扣减DB库存,保存task表做兜底。
第二阶段:数据库的库存扣减和用户领券记录入库、保存用户领券记录到redis、MQ延时删除过期优惠券。
问:为什么要解析总行数?
- 把Excel的行数解析成功之后展示在后管页面上,最后分发完成再弹窗提示分发成功条数,让运营自己来对比。
- 后端在分发完成之后自动校对数据,若全部分发成功,则弹窗:全部分发成功,若没成功,弹窗:还有xxx条数据未分发成功,单击下载未分发成功记录......
问:为什么要批量操作?
原本业务将前置验证逻辑与保存分发记录等放在一起,导致每解析一行数据就需要对Redis和MYSQL进行读写操作,这种情况下大量网络IO会成为性能瓶颈,而且我测试过,5000行数据执行完整体逻辑耗时1min。此外,单次消息消费时间过长可能会导致Broker重发消息导致消息堆积。
因此我把涉及网络IO部分的业务进行批量操作,减少网络IO次数,整个流程只需要一秒,提升接口的响应时间。
问:redis如何扣减库存?
EasyExcel解析每一行数据,redis中扣减库存,根据实际扣减的库存保存用户数据,最后返回一个bitmap。
(查询的结果bitmap我做了一个小优化,将返回的库存是否充足(这是个01值)与暂存用户数,因为我一批是5000条,所以我用一个long类型的第14位保存库存充足与否,前13位保存暂存用户数,(2^13=8192>5000)。这样不需要再请求一次redis去获取暂存用户数,同时可以避免使用字符串作为返回值,后续程序分割字符串带来的耗时。)
返回扣减失败,则保存失败的记录到数据库。返回扣减成功,判断是否足够批量,再去执行实际的优惠券分发逻辑
问:优惠券分发的过程中如何解决超卖和少卖?
解决超卖方案:
并发操作会导致库存超卖,关键就是保证库存扣减的原子性和有序性
第一阶段在redis使用LUA脚本扣减库存,扣减库存失败之后就更新优惠券发放状态,不再解析,
第二阶段在MYSQL行记录锁来确保并发情况下不会多扣,扣减库存时采用乐观机制扣减库存。
还可以加滑块锁对单个库存加锁防止超卖
解决不少卖:
redis扣减成功,但是MQ发送失败、或者库存扣减失败(宕机)
方案一:MQ消息发送前保存一份task任务到DB,后续DB扣减成功之后更新task状态,开后台服务定时轮询新增的task的状态,如果状态不是【已结束】,则拉出来重新消费。
方案二:如果是数据库宕机了这就比较严重了,得先做对账,核对成功保存入库的数据与优惠券库存,如果少卖了就把库存加回去。
保证不多领:在用户领券记录表(redis的Hash数据结构存储)中将用户ID、优惠券ID作唯一联合索引,通过这个唯一索引做幂等。
分发消息的实际消费逻辑中 ,为了防止redis和MYSQL数据不一致问题,需要去数据库扣减库存,根据实际扣减库存量来从redis中拿对应数量的用户信息,然后批量把用户领取优惠券信息保存到数据库与redis缓存
(保存用户领券记录到数据库的过程中,如果批量插入失败就换成单条数据插入,防止频繁报错,如果单挑插入也报错,就说明用户已经领券过了,会被唯一索引(用户ID、优惠券ID、ID)校验住,这是要记录重复领券用户。)
问:redis扣减成功,但是DB扣减发现库存不够,如何解决?
这个时候再扣减库存就没必要了,需要同步的更新redis库存,并且记录分发错误的信息方便后续核对(主要包括优惠券分发任务ID、优惠券ID、保存用户对应Excel的行、错误原因),更新优惠券分发任务的状态为已结束。
问:如何保证数据库的库存和redis库存的数据一致性?
答:
先要回答一致性的种类,然后回答项目当中整个链路是如何保证一致性的。
没有绝对的银弹,需要考虑我们业务中能接受的一致性【强一致性、弱一致性、最终一致性】。
对于强一致性,比如支付场景,就必须进行同步
对于最终一致性,可以有延时,就需要考虑能接受的延时时间
最终一致性方案:
为key设置合理的过期时间:适合并发不高的情况
延时双删:适合并发不高的情况下,不然容易出现缓存击穿和雪崩。
canal监听binlog同步:canal伪装成slave,解析binlog的增量日志,发送MQ去更新redis库存
本项目保证一致性的方案:
对于我们项目中为了有更高的吞吐量会选择保证最终一致性,保证数据库与redis的一致性关键在于不超卖。
使用redis的decr命令扣减库存,并在扣减成功后加滑块锁确保不超卖。
发送MQ信息扣减数据库库存,MQ有重试机制可以保证消息正常消费,并且使用task任务表兜底。
扣减数据库的库存时,使用InnoDB引擎的记录锁(悲观锁),并在SQL中使用乐观机制扣减库存确保不多卖
数据库的库存变更之后,使用canal监听binlog发送消息给MQ,再去同步redis中的库存,如果数据库库存为0,则同步更新redis库存,这里需要去压测得到可以接受的延时时间。
问:压测性能是多少呢?
答:i5 13500HX 16GB ,5000条数据分发耗时1秒。
用户优惠券分发压测数据:i5 13500HX 32GB 本地启动了一个牛券分发、后管服务,使用Faker生成了1百万条数据,分发耗时5min。
用户查询优惠券的压测数据:i5 13500HX 32GB 本地启动了一个牛券结算服务,配置了80个线程循环1000次压测,最终吞吐量是4215
秒杀服务的压测数据:i5 13500HX 32GB 本地启动了一个牛券领取服务,配置了100个线程循环400次压测,最终吞吐量是1520。
问:消息队列的消息需要顺序消费吗?
答:当然要顺序消费,所以我们选择了RocketMQ,它可以在兼顾吞吐量的同时支持顺序消费,因为如果倒数第二批的5000条数据还在执行库存扣减和领券记录保存,最后一批的10条消息也开始处理并且先完成,这就会导致倒数第二批的信息入库失败了,所以要保证消息消费有序。
如何保证有序性:同一个队列中的消息消费是有序的,但是不同队列里消息的消费是无序的。
(1)消息生产者发送消息的时候需要自定义类实现MessageQueueSelector,然后实现select方法,在方法里面,通过对队列数量取模算法来指定把消息投放指定topic下的队列,因为只有单个queue里面才能保证全局有序性。
(2)注册消费者的时候需要选择有序消费模式
(3)申请三把锁:第一把:向Brocker为一个消费者申请一把锁,使得这个只会向该消费者发送消息。第二把:当消息保存到本地的ProcessQueue之后,需要申请MessageQueue锁,确保只有一个线程来消费。第三把锁:对存储消息的ProcessQueue进行加锁,防止重平衡过程中消息的重复消费,重平衡过程中会重新为队列分配消费者,此时需要先解锁ProcessQueue再去解锁MessageQueue。
三、优惠券秒杀活动的流程?
问:你如何设计秒杀框架的?
1、高并发的瞬时流量:
解决办法就是:分布式架构+整个链路中进行限流+缓存预热+库存和购买频次限制:
限流策略:
1、客户端可以随机抛弃一些请求,减轻压力
2、把秒杀活动资源提前存储在CDN上,提升响应速度。例如商品信息,页面的静态资源。
3、nginx做请求转发、负载均衡的时候也可以做用户黑名单处理
4、setinel进行限流、熔断、降级
缓存预热:
可以用本地缓存与分布式缓存做多级缓存来抗高并发,并且可以进行缓存预热。
缓存预热:本地缓存与分布式缓存预热,防止缓存击穿与缓存雪崩
本地缓存预热:应用启动时预热(继承CommandLineRunner)、定时预热(@Scheduled定时任务)、用时预热(根据用户查询的频率预热)、缓存加载器(Caffine)
分布式缓存预热:启动时预热(加载到redis)、redisBloom(快速查询缓存是否存在)、redis Desktop(批量导入缓存)
如何解决本地缓存与redis的数据一致性:
首先需要明确本地缓存存储的应该是一致性不强的数据,对于一致性强的数据应该放在redis中
1、本地缓存设置过期时间,到期之后自动到redis读取数据
2、redis采用发布/订阅机制,redis数据发生变动推送到频道,本地缓存订阅频道做数据更新。
3、redis数据发生变化用MQ更新本地缓存
库存不超卖:
扣减缓存中的库存失败直接返回,并且对库存加锁,确保不超卖(LUA脚本实现)
2、热点数据:热点数据拆分、缓存
热key问题:
多热算热:根据实际场景和服务器的配置决定,与QPS次数、网络带宽占用、CPU资源占用有关
识别热key:事前预测:大促活动前提前预热,或者使用redis实时监测热key redis-cli-hotkeys
处理热key:多级缓存(客户端、CDN、redis、本地缓存)、热key拆分(热key加前缀或者后缀,增加key数量来减少value的大小)、读写分离(如果热key读多写少,做读写分离)。
例如对于抖音的热点事件,通过加后缀将他们拆分到不同的节点存储,每次只给用户推送一部分数据,等过一段时间再推送另外的数据。
3、数据量大:
例如大促活动时用户高频下单,分库分表、加缓存
4、库存的正确扣减:
如何不超卖:库存扣减我们不会直接去扣减DB中的库存,不管是DB的乐观锁还是悲观锁性能都不太好。因此我们先使用LUA脚本扣redis中的缓存,扣减成功之后,再通过MQ发送消息至DB进行库存扣减,并且在Update语句的where条件里使用stock>=1这种乐观机制进行扣减,保证有库存了再扣减。
如何不少卖:redis库存扣减成功,存一下流水在redis的zset中,定期的去比对一段时间的流水记录,如果少了就补库存。
LUA or 分布式锁:
LUA脚本可以保证命令执行的一致性,但是不能保证执行失败之后可以回滚,底层的原理是redis会把LUA脚本包装成一个事务去执行,整个过程只需要两次网络IO,但是redis事务不能回滚。
分布式锁需要先加锁、再校验and扣减库存、释放锁,IO次数多,浪费资源。
5、如何防刷:第一防黄牛、第二防恶意调用
防黄牛:
对于用户频繁请求,对用户ID进行限流
对于黄牛或者盗刷,对IP地址限流
对于有人用代理,直接对接口限流
主要涉及到风控,经过算法分析IP地址,个人信息,行为判断是否是黄牛,然后在nignx(黑名单),setinel(ip地址限流)等进行管控。
6、防止重复下单:
对接口做幂等,一般分为:加锁、判断、更新三步操作。首先加分布式锁(使用外部透传的唯一ID),因为它是trylock的,非阻塞,效率高,数据库层面也需要用唯一索引加锁 。
7、防止对普通交易产生影响:
逻辑隔离:对于秒杀商品、产生的订单进行标记。
物理隔离:前后端服务隔离,数据存储都隔离开。
总结:
高并发流量:分布式架构、限流降级熔断、缓存预热
热key:识别热key、热key处理
商品超卖and少卖:超卖(LUA脚本or滑块锁)、少卖(后台开线程对账,MQ保存消息表确保能消费)
防盗刷:风控得出黑名单(ngix对用户ID、ip、接口限流)
防重复下单:订单ID做幂等
防止对普通交易产生影响:物理隔离or逻辑隔离
问:你的项目里秒杀框架是啥样的?
问:如何处理缓存击穿
缓存击穿就是在一时刻缓存过期导致大量请求访问数据库,把数据库打垮。
不让它在关键节点击穿:不让在高并发访问中过期。进行缓存预热和永不过期(自动续期)。并且内存淘汰策略应该设置为volatile-lfu,只能在设置了过期时间的key中淘汰。
万一要查DB,也要限流查:
1、限制访问数据库的流量。使用分布式双重判定锁(lock)。在请求获取锁之前再查一遍缓存,如果缓存有值则无需获取,拿到锁之后再查一遍缓存。但是在高并发场景下,末尾的请求依然会阻塞很久
2、限制访问数据库的流量,也可以使用性能更高的本地锁进行分流。例如服务部署20个节点,20个节点上去访问数据库,会比所有节点都去拿一把分布式锁性能高很多。
3、对于功能性场景下,我们不使用trylock,这样会导致一时刻只有一个请求成功,用户体验非常不好。
但是对于秒杀场景等用户喜欢占便宜的情况,因为用户请求失败之后会主动刷新,所以在没有限流熔断降级的情况下,可以使用trylock。
问:Sentinel如何限流
配置:
引入maven包
application.yml文件加入QPS限制
qps:
limit:2
接口限流代码:
对Controller的指定接口进行限流,设置限流类型为QPS_LIMIT
@RestController
public class MyController {
@RequestMapping("/hello")
@SentinelResource(value = SentinelRuleConfig.QPS_LIMIT)
public String hello(){
return "hello";
}
}
定义一个全局的限流配置类,可以对不同的接口配置不同的限流规则
每一个规则最重要的是限流的接口、阈值、限流等级(QSP 、并发线程数)
@Component
public class SentinelRuleConfig implements InitializingBean {
@Value("${qps.limit}")
private Integer limit;public final static String QPS_LIMIT = "concurrent_qps_limit";@Override
public void afterPropertiesSet() {initFlowQpsRule(QPS_LIMIT);
}private void initFlowQpsRule(String resource) {List<FlowRule> rules = new ArrayList<>();FlowRule rule1 = new FlowRule();rule1.setResource(resource);rule1.setCount(limit);rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);rules.add(rule1);FlowRuleManager.loadRules(rules);
}
}
四、预约功能的架构
库表:用户预约表(支持多时段多功能预约),字段:用户ID、券ID、存储信息(预约时间、通知方式)、秒杀活动开始时间。
用户预约功能的实现:
将用户预约信息填充至预约表入库:其中使用bitmap整合关键的预约方式与预约时间信息,具体来说采用long类型数据存储,一个预约方式对应12个bit位,一个bit位对应5min时间间隔,也就是支持最多支持提前1h预约。
采用MQ向用户发送预约信息:
redis延时队列,redis延时队列适合短时间(10秒内)的任务确认,但是预约可以提前几天就开始,这样会在redis中积存百万数据,非常耗费内存。
MYSQL延时队列,通过定时任务去数据库拿数据去消费,但是一台8C16G的服务器QPS才5000左右,对于百万计的预约场景下,大量的IO读取容易成为瓶颈,导致消息消费过慢。
MQ延时队列,MQ优势在于它基于磁盘存储而且吞吐量高
MQ的消费者,提醒用户功能的实现:
采用线程池异步发送消息,因为消息发送是IO密集型任务,因此线程池的四个关键配置:核心线程(CPU核2倍)、最大线程(CPU核4倍)、阻塞队列(同步队列,及时发送)、拒绝策略(不拒绝,CallersRunPolicy)。
我们为了提升接口的相应速度,当任务提交到线程池之后就会立刻返回消息消费成功的信息。
已经发送到MQ的预约信息没法撤回,因此我们在执行优惠券信息推送之前会再查一遍数据库看是否已经取消预约信息,但是每次都要查一遍耗费性能,所以加一个布隆过滤器来查看是否取消了该预约消息,使用(用户ID、优惠券ID、提醒时间、提醒类型)四元组来计算hash值
为了防止服务器宕机,线程池任务丢失,我们在向线程池提交任务前把预约信息key放一份到redis延时队列里面进行兜底操作,当线程池执行完任务后到缓存里面删除key。后台线程去不断的去redis延时队列中拿没有执行成功的任务的key重新给用户发送提醒消息,
这种情况下是否需要进行消息幂等:
不需要,两点原因,第一、这是给用户发送预约消息,这种场景下多发送一次问题不大。第二,如果引入幂等,就需要设置和检查redis,这里会存在两次IO,百万级别预约信息会占用过多IO与内存资源。
如何应对高并发流量:
增加节点和临时扩容线程池
用户查询预约信息:
先查缓存,再查数据库,将预约信息放到redis中,过期时间为1min。
查询数据库的过程中,用户可能预约了多个店家的优惠券秒杀预约信息,因此涉及到分库批量查询,那么就根据店铺编号计算出分片键,按照分片键分类查询。
用户删除预约消息:
验证优惠券,判断秒杀活动是否已经开始(开始了就直线显示预约信息过期)
查询用户预约记录(根据用户ID、优惠券ID)
计算取消预约信息所对应的时间与提醒方式
判断预约记录中是否包含这个提醒节点,如果不包含则抛异常,包含了则异或更新
判断是否还有预约信息剩余,如果没有则直接删除记录。
五、用户优惠券的锁定、核销、退还
整体的逻辑相似,通过分布式锁与事务确保优惠券状态变更的原子性和一致性。
优惠券结算表:记录优惠券使用状态,包括订单ID、用户ID、优惠券ID、结算单的四种状态(锁定、已取消、已退款、已支付)需要分库分表,但是没有优惠券的表数量那么多,因为券总是领的多用的少。
锁定优惠券:用户在订单结算的时候使用了优惠券,就会创建优惠券结算单,并且变更优惠券状态从“未使用” -> “锁定中”
(1)获取优惠券的分布式锁防止并发事务操作同一张优惠券,key就是优惠券ID+主键ID,使用trylock,如果加锁失败则抛异常。
(2)查询当前是否存在优惠券结算表,如果有,就需要校验结算单的状态是已经锁定或者已经使用,这两种状态下不能重复创建结算单。如果是已退款或已取消则可以继续创建。
(3)优惠券状态校验,因为分布式锁是拿到了优惠券的操作权力,但是还需要校验优惠券是不是已经锁定或者已支付。
(4)根据优惠券类型校验折扣金额。商品券or平台券
商品券:核对商品ID,计算金额,核对与前端计算的金额是否相同。
平台券:核对店铺编号,计算金额。
(5)一个事务下更新->优惠券结算单状态(锁定状态)、优惠券状态(锁定状态)
(6)删除redis缓存中用户优惠券(key就是优惠券ID+主键ID,因为不同优惠券主键ID不同),防止用户看到并想再次使用。
核销优惠券:核销就是使用,实际就是使用优惠券的过程。
获取优惠券的分布式锁,防止多用
同一个事务下更改优惠券结算单的状态(已支付)、优惠券的状态(已使用)
退还优惠券:
获取优惠券的分布式锁,防止多用
同一个事务下更改优惠券结算单的状态(已退款)、优惠券的状态(未使用)
将优惠券重新放入缓存,用户可以继续使用。
查询用户可用与不可用优惠券以及使用线程池优化:
redis中用户ID列表对应一个sorted set集合(便于后续快速查询券是否过期),按照优惠券是否包含指定商品进行数据分区,分为包含和不包含。
使用ComputableFuture对两个集合中的优惠券进行并行计算优惠金额的任务进行异步编排。
使用.thenRun()这个回调函数对计算完的金额进行从大到小的排序再返回。
使用.join()阻塞等待任务完成。
期间计算优惠金额和将可用或者不可用优惠券整理的时候使用线程池,因为是CPU密集型,所以核心线程和最大线程都是CPU核数,任务队列为同步队列,拒绝策略为CallersRunPolicy()。
由于存储可用or不可用优惠券集合存在并行写入,所以使用Collections.synchronizedList(new ArrayList<>())将集合转化为线程安全。不使用copyonwriteArayList的原因是这个集合为了实现线程安全会在每次操作的把底层数据都复制一遍,占内存和CPU资源,因此它适合读多写少的情况。
业务代码中批量代码处理优化idea--线程池:
并行处理任务可以提升接口的响应速度,但是引入线程池不一定总会提升性能,多线程之间的切换与上下文切换,数据同步也会占用资源。
六、其他问题
问:项目的整体框架,要简洁概略的描述项目的流程以及各个部分的亮点。
答:
项目整体分为分发模块、后管模块、引擎模块、结算模块。
后管模块:面向商家,负责优惠券的创建、以及目标用户的优惠券分发任务
分发模块:面向商家,负责执行百万级别优惠券的分发任务
引擎模块:面向用户,为用户提供优惠券查询、兑换、秒杀以及秒杀活动预约等功能,并在在
结算模块:面向用户,用户创建订单时进行优惠券的锁定、核销与退还。为用户提供结算时可用与不可用优惠券的查询功能,并且计算订单的结算金额。
问:如何进行用户鉴权
coockie:存储客户端与服务端会话状态,由服务端生成,保存在客户端
session:存储客户端与服务端会话状态,由服务端生成,保存在服务端,sessionID放在cookie头里面交给客户端,下一次访问的时候根据sessionID拿到对应session。
二者的缺点:cookie和session不能跨域。
JWT:一种认证授权机制,用户输入用户名和密码,服务端认证成功后可以根据密钥对用户认证信息加密,然后生成对应的jason格式的token。
JWT的优势:
可以跨域(放在HTTP的Authorization里,而不是放在coockie,因为coockie不能跨域)
它是无状态的,本身就携带了用户信息。
他可以解决传统基于Session的身份验证方式存在的跨域和服务器存储问题。
它可以避免CSRF的攻击。
JWT的缺点:
主动过期问题、权限更新问题
难以续签
JWT组成: Header(格式、加密算法HS265、RS265)、Payload(用户自定义字段)、Signature(Header的Base64编码+“.”+Payload的Base64编码+密钥,的整体进行加密得到的字段)
JWT与token的区别:
token只是一串鉴定身份的字符串,JWT存储用户的认证信息。
token还需要再查一遍数据库才能验证有效性,JWT直接使用密钥进行解码即可,解码成功则有效。
JWT的组成:
public class LoginVo implements Serializable {
private static final long serialVersionUID = 6711396581310450023L;
//...省略部分业务字段
/** 9 * token令牌 过期时间默认15day 10 /
private String jwt;
/*
* 刷新token 过期时间可以设置为jwt的两倍,甚至更长,用于动态刷新token
/
private String refreshJwt;
/*
* token过期时间戳
*/
private Long tokenPeriodTime;
}
无感知刷新token方案:
JWT返回的信息里包含jwt、refreshJwt、jwt时间戳。
这种情况需要区分活跃用户和非活跃用户:
对于活跃用户,不能操作到一半就跳转到登录页,这样体验不好,需要无感知刷新token。
双token方案:
1、用户登陆成功返回token、refresh token、token请求接口、refresh请求接口
2、客户端请求服务端,返回401,则是token过期了,则请求refresh token接口获取新的token
3、如果返回成功就更新本地token和refresh token,如果返回失败就跳转到登陆界面。
HTTP请求的格式是,主要有一个grant_type=refresh_token
POST/oauth/tokenHTTP/1.1
Host:authorization-server.com
grant_type=refresh_token
&refresh_token=xxxxxxxxxxx
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
对于不活跃用户,直接定向到登录页。
动态修改用户权限:
白名单方案:把所有token都记录在redis里,并且加上状态信息。但这样会占用很多的内存
黑名单方案:只把用户退出登陆,或者手动失效的token记录下来。
但是无法满足【用户想要失效所有令牌】的需求,有一种办法是在redis中记录一个时间节点,验证的时候这个时间节点前的token全部失效。
短时间的Access token:accessToken设置更短的时间,快过期了就用refreshToken去拿到新的accessToken。这种方式比较符合JWT设置的初衷,但是可能会使用refreshToken去频繁获取accesToken。
用户权限信息的实时刷新问题:
1、失效所有令牌(某个时间戳后所有JWT全部失效),并且使用refreshToken去获取新的AccessToken
2、将用户权限信息保存在白名单里,该权限就修改白名单就行。
问:什么是网关?
网关相当于是服务的入口,他主要有三个职责:
1、统一入口:集中接受客户端的请求(HTTP)
2、路由转发:把请求转发到对应的服务中
3、鉴权、限流、日志:
4、协议转换:HTTP转gRPC
问:项目如何做幂等
幂等产生的原因:网络波动、用户重复操作、分布式消息消费、重试机制(消息队列)
幂等方案:全局唯一ID做分布式锁、数据库唯一索引、状态机控制
幂等的步骤:一锁、二判、三更新
幂等的类型:请求幂等、业务幂等
请求幂等:请求参数一样、结果一样
请求幂等示例:创建优惠券
一锁:使用互斥分布式锁,key:前缀+商家ID+参数MD5,要加过期时间防止死锁
二判:保存一份key放到redis,设置过期时间,创建优惠券前查看缓存是否有key,如果有则创建
三更新:如果没有执行,则执行任务返回结果
业务幂等:相同业务请求要返回相同的最终状态,在没有拿到最终状态前,每次请求都得执行正常逻辑。
比如支付请求,如果返回过程出现系统异常信息,用户重试,继续调用接口就会继续进行处理逻辑,直到返回一个确切的支付成功或者支付失败的结果。
业务幂等示例:消息消费
DB唯一索引(兜底)+使用消息防重表:在一个事务下,保存用户领券记录、保存任务表task,再去发送MQ消息。用户领券记录可唯一索引可以校验住,task表可以防止MQ消费失败,也可以防止MQ重复消费。
使用状态机做幂等:
对于有先后顺序的行为用int数据大小来标注先后顺序,SQL更新的时候的where条件要加上状态
Map<State,Map<Action,StateTransition>>
订单的当前状态对应不同的后续执行动作,例如待支付可以对应支付或者取消
支付对应支付,但是行为动作就是返回一个“已支付,请勿重复支付”。
1、用户ID+优惠券ID做为key存入redis
2、布隆过滤器
3、token
问:消息堆积怎么处理?
答:
(1)增加消费者数量:开更多实例来消费,开线程池来消费
(2)降低生产者生产速度
(3)清理过期消息或者一些业务一直无法成功的消息,对一些消息进行业务评估,如果不太重要可以抛弃
(4)调整MQ的参数,如消息消费模式和消息拉取的时间
问:布隆过滤器
(1)为什么使用布隆过滤器,优缺点?
布隆过滤器是由一组无偏hash函数与二进制数组组组成,可以高效的判断元素是否存储在集合当中。从而减轻直接查询缓存和数据库带来的压力。
优点:时间复杂度低、保密性强(本身不存储元素)、占用内存小(和误判率有关的)。缺点:有误判率、不方便删除元素。
(2)初始化变量?
初始化需要给定预估添加的元素数量和误判率,这样就会生成位数组的长度和哈希函数的个数。
一亿个元素,误判率设为千分之一,占内存171MB
(3)添加、验证元素是否存在
根据key计算多个hash,对位数组长度取模找到索引。
(4)redis布隆过滤器
命令bf.madd()、bf.exit()、bf.mexit()
(5)redisson客户端在本地计算哈希值,然后把哈希值转化为偏移量发送给redis。
(6)创建新优惠券的时候更新一下布隆过滤器即可。
(7)假阳性的因素:位数组大小、哈希函数个数、当前存储的元素个数。
(8)300亿数据、千分之一的误判率使用布隆过滤器内存大概在50GB
问:如何处理缓存穿透
布隆过滤器+空值缓存+分布式锁
问:如何处理缓存雪崩
答:缓存雪崩指的是redis中大批热点数据过期,请求直接打到数据库。或者,redis直接挂了,所有请求全部打到数据库。
key设置随机的过期时间,不要短时间内同时过期
做高可用架构,redis集群
redis真挂了,做服务降级,可以使用本地缓存+限流。
问:项目Redis存储的内容
Key Value
优惠券ID Hash:stock:库存值
用户已领取优惠券(用户ID) 用户已领取的优惠券数量
临时存储的用户发券信息 用户ID+excel行号+优惠券ID
问:用户优惠券秒杀方案设计
秒杀场景下方案的设计:
v1: 前置校验(redis是否有优惠券缓存、秒杀时间是否开启、用户领券次数是否已达上限)->LUA扣减redis库存,保存用户ID与excel行号->扣减DB库存->新增DB中用户领券记录->新增redis中用户领券记录(先写后查防止丢数据)->发MQ延时消息删除用户缓存中的优惠券。
v2:前置校验(redis是否有优惠券缓存、秒杀时间是否开启、用户领券次数是否已达上限)->LUA扣减redis库存->扣减DB库存->新增DB中用户领券记录---->Canal监听binlog---->新增redis中用户领券记录(先写后查防止丢数据)->发MQ延时消息删除用户缓存中的优惠券。
问题:
扣减redis库存与扣减DB库存(做事务)在一个环节内将导致接口响应时间过长。
使用canal解耦,但是canal挂了怎么办,如何监听并通知?
v3:前置校验(redis是否有优惠券缓存、秒杀时间是否开启、用户领券次数是否已达上限)->LUA扣减redis库存
------->MQ------->扣减DB库存->新增DB中用户领券记录->新增redis中用户领券记录(先写后查防止丢数据)->发MQ延时消息删除用户缓存中的优惠券。
提升主线程的响应速度,使用MQ解耦,将操作数据库的行为异步进行。
存在的问题:
Redis极端场景下丢数据:1、持久化丢数据,不管是RDB还是AOF都或多或少丢数据,可能导致超卖问题。2、主从复制过程也会导致丢数据。
解决办法:
1、在流量低的场景下,可以使用同步扣减库存
2、在高并发流量下,如果秒杀的量很少,也可以使用同步扣减库存,不会造成很大的延时
3、在高并发流量下,如果秒杀的量真的很大,但是很多redis的持久化和主从复制都是异步的,但是需要保证强一致性,那么在redis成功扣减库存之后,给前端响应一个等待的场景,然后再等数据库的缓存扣减成功之后,再向前端返回成功扣减。
但是这种方法适用于强绑定的场景,也就是说用户必须使用这个APP,如果不是强绑定,也可以考虑用一些补偿机制。
本项目的方案
对于一般场景:redis 校验+扣减库存、DB扣减+新增DB领券记录和redis领券记录
对于高并发秒杀场景:redis 校验+扣减库存-->MQ-->DB异步扣减+新增DB订单记录和redis订单记录
对于一致性要求高的场景方案一:redis部分结束之后前端界面显示等待,直到DB扣减库存+新增订单和redis记录,再返回给前端支付成功的消息。
对于一致性要求高的场景方案二:MQ发送消息保存task表兜底,后台定时任务轮询task表,确保消息消费成功。
一般缓存与数据库不一致的情况如何处理?
方案一:延时双删。先删除缓存、再更新数据库,因为可能在更新数据库之前有读数据库的请求,在更新数据库之后又更新了缓存,造成脏页情况,因此在更新数据库之后可以采用消息队列监听binlog删除缓存。
方案二:使用中间件(例如Canal)去监听数据库的binlog,获取需要删除的缓存的信息,然后另开一个服务订阅数据去删除缓存数据。
方案三:在流量不高的情况,可以给缓存设置一个合理的过期时间,到期之后再去数据库中读最新数据。
问:责任链模式
责任链是一种设计模式,它是为了将请求方和多个处理方进行解耦,每个处理方成为一个节点或者处理器,每一个处理器都包含自身的处理逻辑以及指向下一个处理器的指针,只有当前处理器通过之后才会传递到后面的处理器。我们将处理器串联起来形成一个链条,请求在链条中依次传递,依次被多个处理器处理。
应用场景:身份信息校验、过滤器链、异常处理器。
例如在我的项目中,对于商家在后管平台创建优惠券,我的后管系统中就是用一个责任链来处理商家创建责任链的请求。一共有三个处理器,每个处理器都继承order类,为每个节点固定一个顺序。第一个节点是优惠券各项信息非空,第二个节点就是优惠券的基本数据关系是否正确(例如全店通用的优惠券不能指定商品类型,指定商品的优惠券但是商品类型处为空),第三个节点是验证优惠券中的数据是否真实,(例如调商品中控去查看是否真的有这个商品编号)
问:为什么选择biz-log
mzt-biz-log:支持Springboot,基于注解的可使用变量、可以自定义函数的通用操作日志组件。他是通过AOP切面类实现的,具体来说,他使用的是AOP拦截器,在方法执行前解析模板,然后执行方法,根据方法执行是否成功来选择成功模板或者失败模板,再填充对应模板。
mzt-biz-log 以下简称biz-log。有如下优点:
(1)使用注解记录日志,注解里的属性就定义了模板,这样可以和业务代码进行解耦。
(2)支持成功和失败的日志模板配置,并且模板是动态的,再业务执行之后根据具体情况填充日志信息,比如可以填充优惠券ID(券ID是插入数据库之后自动生成的),使用spel表达式构成动态模板。
(3)提供了自定义函数,可以自定义一些解析逻辑,比如根据优惠券类型type去拿到对应的value,填充日志。
(3)可以与业务ID绑定,比如说优惠券ID,通过spEL表达式将bizNo属性与优惠券ID绑定,在日志中打印出来(优惠券ID是插入数据库后自动生成的,可以把优惠券ID存到LogRecordContext里面,再用spEL表达式拿到)
问:幂等处理的几种方案
防止用户反复签到:外部透传ID做幂等,外部透传ID就是年月日
问:MQ消息发送失败了怎么办,结合项目作答
最好的办法:task任务表兜底
问:优惠券库存查询优化的目标是什么?
问:分发场景or秒杀场景下redis扣减成功,但MYSQL保存失败
此时以数据库的库存为基准,同步地将redis中的库存置0,并且保存分发失败的记录。
如果确实有用户扣减成功了,在同步完reids库存之后直接关闭活动,这一个用户可能就需要平台补偿了,但是补偿的时候也得分责任,MYSQL出问题到底是谁的责任。
问:用户过期优惠券的删除
1、用户查询优惠券的时候判断是否过期,如果过期就不返回,然后异步更新数据库的【用户领券信息】。
问:更好的秒杀方案
方案一:直接进行数据扣减
为了保证数据精确,使用阿里的RDS云数据库,内置Inventory Hint技术可以让数据库抗高并发。
方案二:压力分摊+Redis+MQ
redis存储数据:
token:用户ID+优惠券ID+提交次数编号
rediLUA脚本流程:判断是否重复提交--判断库存是否充足--记录流水(JsonObject)
问:优惠券分发接口如何得知分发成功?
答:分发结束之后将优惠券分发任务设置为已完成,而且结束之后可以异步回调短信、邮件通知接口
问:优惠券分发任务中批量插入用户领券记录为什么快?
1、SQL语句模板只需要解析一次,后续只需要替换参数值即可。
2、批量处理能减少磁盘IO
3、批处理主要体现在redolog刷盘的层面,commit fsync,默认是commit一次同步刷一次,还有其他两种策略一般不会使用的。刷盘1000次就是1000次磁盘IO,合成一次只有一次磁盘IO。
问:优惠券分发任务中批量插入用户领券记录如何处理跨库问题?
使用MYSQL自带XA事务
结合ShardingSphere和Seata完成分布式事务。
问:LUA脚本
LUA脚本会将执行命令打包给redis执行,脚本的解析在redis中进行。
LUA脚本强原子性指的是脚本整体执行过程不会中断,但是如果脚本内部命令执行出错,不会回滚,只是后续的命令不会执行。
与事务的区别:
事务是弱原子性(中间可能穿插其他命令),MUTI开启事务,EXEC批量顺序执行。
问:牛券项目的性能优化
优惠券查询优化:
● 批量查询使用redis的pipeline功能,减少网络IO
● 查询的时候需要计算优惠券的优惠金额,这里我是用线程池计算的,因为是CPU密集型,所以给的核心线程是CPU,最大线程是CPU核数+1,具体的参数得上线之后调优
秒杀优化:
redis库存分桶,并且要注意实时检测桶的QPS防止数据倾斜问题,
慢查询优化:
问:如何保证布隆过滤器与数据库的一致性
创建优惠券的时候在布隆过滤器中加入优惠券的信息,但是布隆过滤器不支持删数据。
需要删除数据,可以使用布谷鸟过滤器与带计数的布隆过滤器(在布隆过滤器中传入一个计数器)
存储:redis的存储在redis,可以持久化。本地Guava布隆过滤器的存储在内存,不能持久化
问:RocketMQ消息被消费者消费了,但是消费失败了怎么办
消息监听器监听到异常之后会重试
问:MQ如何保障消息不丢失?
生产者:事务里面保存task任务表,等消息消费之后再更新任务状态,后台开定时任务轮询task表
消费者:MQ本身有重试机制,并且可以根据消息key加分布式锁做幂等
问:库存扣减的性能优化
问:用户优惠券分发如何得知消息分发结果
异步回调站内信或者邮件性质告知商户优惠券的实际分发结果,实际的分发结果包含:成功分发的条数,分发失败数,以及分发失败的查询与下载。
问:优惠券分发过程中批量插入用户领券记录为什么快?如何处理跨库事务?
为什么快:
1、SQL只需要预编译一次,后续替换参数即可
2、批量可以减少网络IO
3、批量保存的情况下做事务,只需要手动提交一次,这样redolog日志刷盘只需要刷一次,大大减少磁盘IO。
如何处理跨库事务:
结合SharedingSphere和Seata做分布式事务。
问:秒杀活动如何增加库存?
使用canal监听binlog,将数据同步到redis;并且一般是第一批活动结束,然后补库存,再重新上线活动。
问:如何进行慢SQL优化?
1、介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概6秒钟)
当时用户查询查询可用优惠券信息时非常慢,80个线程循环1000次大概5min。
2、我们系统中当时采用了运维工具( Skywalking ),可以监测出哪个接口,最终因为是sql的问题
用Skywalking检测数据库查询接口,最终发现是这个接口响应很慢
3、在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段)
执行 SHOW VARIABLES LIKE 'slow_query_log';查看是否开启了慢查询日志
开启慢查询日志,设置慢查询阈值,设置慢查询日志的路径
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2; -- 设置慢查询阈值(单位:秒)
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log'; -- 指定日志文件路径
4、定位到select语句,使用explain发现type是index,extra是Using Index+Using Where(全索引扫描),然后发现是最左前缀法则使用错误。
4、上线前的压力测试,观察相关的指标 tps / qps / rt / io / cpu
系统监控:基于 arthars 运维工具、promethues 、skywalking mysql 慢日志: 会影响一部分系统性能 慢 SQL 处理 由宏观到微观
1、检查 系统相关性能 top / mpstat / pidstat / vmstat 检查 CPU / IO 占比,由进程到线程
2、检查 MySQL 性能情况,show processlist 查看相关进程 | 基于 MySQL Workbench 进行分析
3、检查 SQL 语句索引执行情况,explain 关注 type key extra rows 等关键字
4、检查是否由于 SQL 编写不当造成的不走索引,例如 使用函数、not in、%like%、or 等
5、其他情况: 深分页、数据字段查询过多、Join 连表不当、业务大事物问题、死锁、索引建立过多
6、对于热点数据进行前置,降低 MySQL 的压力 redis、mongodb、memory
7、更改 MySQL 配置 , 处理线程设置一般是 cpu * 核心数 的 2 倍,日志大小 bin-log
8、升级机器性能 or 加机器
9、分表策略,分库策略 10、数据归档
问:常见的Linux指令
系统性能相关:top(查看进程的资源使用情况)、mpstat(CPU核心的负载情况)、pidstat(单个进程的资源使用情况)、vmstat(虚拟内存的使用情况)、ps(系统进程的资源占用情况)
文件:touch(创建文件)、复制(cp)、mv(移动文件或者重命名文件)、find或者locate(在指定目录中查找文件)、rm(移除文件、目录)
问:秒杀情况下redis顶不住怎么办?
1、前端优化:
前端可以做随机丢弃请求来减少请求量,或者要求用户进行简单验证来分散请求量。
CDN存储静态资源,减少对redis的访问
用消息队列削峰来平滑流量
2、redis层:提升处理能力
redis做cluster集群部署,并将库存分片。
对redis的操作批量化处理,减少网络IO。
问:为什么优惠券库存使用Hash结构而不是String结构
因为String底层是SDS,如果对其增删它需要先转化成整数再进行运算,这不是原子性的。
而Hash可以进行HINCRBY,这是原子性的。
问:用户券表等需要分库分表的,主键ID如何取
问:redis扣减库存成功,但MYSQL扣减库存失败如何处理?
MYSQL扣减数据库失败就同步更新redis库存为0,并且关闭活动,后台进行对账,查看是多发还是少发。
问:用户领取优惠券之后如何将过期优惠券删除?
方案一:RocketMQ定时任务删除,消息内容:用户ID+优惠券ID
方案二:用户查询优惠券的时候判断是否过期,如果过期就删除redis的优惠券,异步更新数据库。
方案三:后台开线程异步扫数据库中用户领取的优惠券是否过期
问:项目中如何使用分布式锁
创建优惠券做幂等。
解决缓存击穿与穿透时做限流
防止超卖加滑块锁
问:Canal做主从复制的原理
https://developer.aliyun.com/article/1179561
Canal的核心原理在于模拟MYSQL slave的交互协议,伪装成MYSQL slave向MYSQL master发送dump协议,从而接受并解析binlog日志,实现增量同步。
具体同步分为三个线程:Master的dump线程,Slave的IO线程、SQL线程。
Canal架构:
在一个Canal实例中只有启动instance才能进行数据同步,一个canal实例中可以创建多个canal instance实例,每个canal instance 可以视为一个MYSQL实例。
instance模块:
eventParser: 数据源接入,模拟 slave 协议和 master 进行交互,协议解析;
eventSink: Parser 和 Store 链接器,并且进行数据过滤、加工、分发的工作;
eventStore: 数据存储;
metaManager: 增量订阅&消费信息管理器;
HA(高可用)机制:借助第三方的分布式同步协调服务实现高可用,Zookeeper。