5.0 store模块

 2018-10-08 23:14:58  8,328 7

1 store模块简介

store模块用于binlog事件的存储 ,目前开源的版本中仅实现了Memory内存模式。官方文档中提到"后续计划增加本地file存储,mixed混合模式”,这句话大家不必当真,从笔者最开始接触canal到现在已经几年了,依然没有动静,好在Memory内存模式已经可以满足绝大部分场景。

store模块目录结构如下,该模块的核心接口为CanalEventStore

以下是相关类图:

其中MemoryEventStoreWithBuffer就是内存模式的实现,是我们分析的重点,其实现了CanalEventStore接口,并继承了AbstractCanalStoreScavenge抽象类。需要注意的是,AbstractCanalStoreScavenge这个类中定义的字段和方法在开源版本中并没有任何地方使用到,因此我们不会对其进行分析。

MemoryEventStoreWithBuffer的实现借鉴了Disruptor的RingBuffer。简而言之,你可以把其当做一个环形队列,如下:

针对这个环形队列,canal定义了3类操作:Put、Get、Ack,其中:

  • Put 操作:添加数据。event parser模块拉取到binlog后,并经过event sink模块过滤,最终就通过Put操作存储到了队列中。

  • Get操作:获取数据。canal client连接到canal server后,最终获取到的binlog都是从这个队列中取得。

  • Ack操作:确认消费成功。canal client获取到binlog事件消费后,需要进行Ack。你可以认为Ack操作实际上就是将消费成功的事件从队列中删除,如果一直不Ack的话,队列满了之后,Put操作就无法添加新的数据了。

对应的,我们需要使用3个变量来记录Put、Get、Ack这三个操作的位置,其中:

  • putSequence:  每放入一个数据putSequence +1,可表示存储数据存储的总数量

  • getSequence:  每获取一个数据getSequence +1,可表示数据订阅获取的最后一次提取位置

  • ackSequence: 每确认一个数据ackSequence + 1,可表示数据最后一次消费成功位置

另外,putSequence、getSequence、ackSequence这3个变量初始值都是-1,且都是递增的,均用long型表示。由于数据只有被Put进来后,才能进行Get;Get之后才能进行Ack。 所以,这三个变量满足以下关系:

  1. ackSequence <= getSequence <= putSequence

如果将RingBuffer拉直来看,将会变得更加直观:

通过对这3个位置进行运算,我们可以得到一些有用的信息,如:

计算当前可消费的event数量:

  1. 当前可消费的event数量 = putSequence - getSequence

计算当前队列的大小(即队列中还有多少事件等待消费):

  1. 当前队列的大小 = putSequence - ackSequence

在进行Put/Get/Ack操作时,首先都要确定操作到环形队列的哪个位置。环形队列的bufferSize默认大小是16384,而这3个操作的位置变量putSequence、getSequence、ackSequence都是递增的,显然最终都会超过bufferSize。因此必须要对这3个值进行转换。最简单的操作就是使用%进行取余。

举例来说,putSequence的当前值为16383,这已经是环形队列的最大下标了(从0开始计算),下一个要插入的数据要在第16384个位置上,此时可以使用16384 % bufferSize = 0,因此下一个要插入的数据在0号位置上。可见,当达到队列的最大下标时,再从头开始循环,这也是为什么称之为环形队列的原因。当然在实际操作时,更加复杂,如0号位置上已经有数据了,就不能插入,需要等待这个位置被释放出来,否则出现数据覆盖。

canal使用的是通过位操作进行取余,这种取余方式与%作用完全相同,只不过因为是位操作,因此更加高效。其计算方式如下:

  1. 操作位置 = sequence & (bufferSize - 1)

需要注意的是,这种方式只对除数是2的N次方幂时才有效,如果对于位运算取余不熟悉,可参考:https://blog.csdn.net/actionzh/article/details/78976082

在canal.properties文件中定义了几个MemoryEventStoreWithBuffer的配置参数,主要用于控制环形队列的大小和存储的数据可占用的最大内存,如下:

  1. canal.instance.memory.buffer.size = 16384
  2. canal.instance.memory.buffer.memunit = 1024
  3. canal.instance.memory.batch.mode = MEMSIZE

其中:

canal.instance.memory.buffer.size:

表示RingBuffer队列的最大容量,也就是可缓存的binlog事件的最大记录数,其值需要为2的指数(原因如前所述,canal通过位运算进行取余),默认值为2^16=16384。

canal.instance.memory.buffer.memunit:

表示RingBuffer使用的内存单元, 默认是1kb。和canal.instance.memory.buffer.size组合决定最终的内存使用大小。需要注意的是,这个配置项仅仅是用于计算占用总内存,并不是限制每个event最大为1kb。

canal.instance.memory.batch.mode:

表示canal内存store中数据缓存模式,支持两种方式:

  • ITEMSIZE : 根据buffer.size进行限制,只限制记录的数量。这种方式有一些潜在的问题,举个极端例子,假设每个event有1M,那么16384个这种event占用内存要达到16G左右,基本上肯定会造成内存溢出(超大内存的物理机除外)。

  • MEMSIZE : 根据buffer.size  * buffer.memunit的大小,限制缓存记录占用的总内存大小。指定为这种模式时,意味着默认缓存的event占用的总内存不能超过16384*1024=16M。这个值偏小,但笔者认为也足够了。因为通常我们在一个服务器上会部署多个instance,每个instance的store模块都会占用16M,因此只要instance的数量合适,也就不会浪费内存了。部分读者可能会担心,这是否限制了一个event的最大大小为16M,实际上是没有这个限制的。因为canal在Put一个新的event时,只会判断队列中已有的event占用的内存是否超过16M,如果没有,新的event不论大小是多少,总是可以放入的(canal的内存计算实际上是不精确的),之后的event再要放入时,如果这个超过16M的event没有被消费,则需要进行等待。

在canal自带的instance.xml文件中,使用了这些配置项来创建MemoryEventStoreWithBuffer实例,如下:

  1. <bean id="eventStore" class="com.alibaba.otter.canal.store.memory.MemoryEventStoreWithBuffer">
  2. <property name="bufferSize" value="${canal.instance.memory.buffer.size:16384}" />
  3. <property name="bufferMemUnit" value="${canal.instance.memory.buffer.memunit:1024}" />
  4. <property name="batchMode" value="${canal.instance.memory.batch.mode:MEMSIZE}" />
  5. <property name="ddlIsolation" value="${canal.instance.get.ddl.isolation:false}" />
  6. </bean>

这里我们还看到了一个ddlIsolation属性,其对于Get操作生效,用于设置ddl语句是否单独一个batch返回(比如下游dml/ddl如果做batch内无序并发处理,会导致结构不一致)。其值通过canal.instance.get.ddl.isolation配置项来设置,默认值为false。

2 CanalEventStore接口

通过前面的分析,我们知道了环形队列要支持三种操作:Put、Get、Ack,针对这三种操作,在CanalEventStore中都有相应的方法定义,如下所示:

com.alibaba.otter.canal.store.CanalEventStore

  1. /**
  2. * canel数据存储接口
  3. */
  4. public interface CanalEventStore<T> extends CanalLifeCycle, CanalStoreScavenge {
  5. //==========================Put操作==============================
  6. /**添加一组数据对象,阻塞等待其操作完成 (比如一次性添加一个事务数据)*/
  7. void put(List<T> data) throws InterruptedException, CanalStoreException;
  8. /**添加一组数据对象,阻塞等待其操作完成或者时间超时 (比如一次性添加一个事务数据)*/
  9. boolean put(List<T> data, long timeout, TimeUnit unit) throws InterruptedException,
  10. CanalStoreException;
  11. /**添加一组数据对象 (比如一次性添加一个事务数据)*/
  12. boolean tryPut(List<T> data) throws CanalStoreException;
  13. /**添加一个数据对象,阻塞等待其操作完成*/
  14. void put(T data) throws InterruptedException, CanalStoreException;
  15. /**添加一个数据对象,阻塞等待其操作完成或者时间超时*/
  16. boolean put(T data, long timeout, TimeUnit unit) throws InterruptedException, CanalStoreException;
  17. /** 添加一个数据对象*/
  18. boolean tryPut(T data) throws CanalStoreException;
  19. //==========================GET操作==============================
  20. /** 获取指定大小的数据,阻塞等待其操作完成*/
  21. Events<T> get(Position start, int batchSize) throws InterruptedException, CanalStoreException;
  22. /**获取指定大小的数据,阻塞等待其操作完成或者时间超时*/
  23. Events<T> get(Position start, int batchSize, long timeout, TimeUnit unit) throws
  24. InterruptedException,CanalStoreException;
  25. /**根据指定位置,获取一个指定大小的数据*/
  26. Events<T> tryGet(Position start, int batchSize) throws CanalStoreException;
  27. //=========================Ack操作==============================
  28. /**删除{@linkplain Position}之前的数据*/
  29. void ack(Position position) throws CanalStoreException;
  30. //==========================其他操作==============================
  31. /** 获取最后一条数据的position*/
  32. Position getLatestPosition() throws CanalStoreException;
  33. /**获取第一条数据的position,如果没有数据返回为null*/
  34. Position getFirstPosition() throws CanalStoreException;
  35. /**出错时执行回滚操作(未提交ack的所有状态信息重新归位,减少出错时数据全部重来的成本)*/
  36. void rollback() throws CanalStoreException;
  37. }

可以看到Put/Get/Ack操作都有多种重载形式,各个方法的作用参考方法注释即可,后文在分析MemoryEventStoreWithBuffer时,将会进行详细的介绍。

这里对 get方法返回的Events对象,进行一下说明:

com.alibaba.otter.canal.store.model.Events

  1. public class Events<EVENT> implements Serializable {
  2. private static final long serialVersionUID = -7337454954300706044L;
  3. private PositionRange     positionRange    = new PositionRange();
  4. private List<EVENT>       events           = new ArrayList<EVENT>();
  5. //setters getters and toString
  6. }

可以看到,仅仅是通过一个List维护了一组数据,尽管这里定义的是泛型,但真实放入的数据实际上是Event类型。而PositionRange是protocol模块中的类,描述了这组Event的开始(start)和结束位置(end),显然,start表示List集合中第一个Event的位置,end表示最后一个Event的位置。

Event的定义如下所示 :

com.alibaba.otter.canal.store.model.Event

  1. public class Event implements Serializable {
  2. private static final long serialVersionUID = 1333330351758762739L;
  3. private LogIdentity       logIdentity;                            // 记录数据产生的来源
  4. private CanalEntry.Entry  entry;
  5. //constructor setters getters and toString
  6. }

其中:CanalEntry.Entry和LogIdentity也都是protocol模块中的类:

  • LogIdentity记录这个Event的来源信息mysql地址(sourceAddress)和slaveId。

  • CanalEntry.Entry封装了binlog事件的数据

3 MemoryEventStoreWithBuffer

MemoryEventStoreWithBuffer是目前开源版本中的CanalEventStore接口的唯一实现,基于内存模式。当然你也可以进行扩展,提供一个基于本地文件存储方式的CanalEventStore实现。这样就可以一份数据让多个业务费进行订阅,只要独立维护消费位置元数据即可。然而,我不得不提醒你的是,基于本地文件的存储方式,一定要考虑好数据清理工作,否则会有大坑。

如果一个库只有一个业务方订阅,其实根本也不用实现本地存储,使用基于内存模式的队列进行缓存即可。如果client消费的快,那么队列中的数据放入后就被取走,队列基本上一直是空的,实现本地存储也没意义;如果client消费的慢,队列基本上一直是满的,只要client来获取,总是能拿到数据,因此也没有必要实现本地存储。

言归正传,下面对MemoryEventStoreWithBuffer的源码进行分析。

3.1 MemoryEventStoreWithBuffer字段

首先对MemoryEventStoreWithBuffer中定义的字段进行一下介绍,这是后面分析其他方法的基础,如下:

  1. public class MemoryEventStoreWithBuffer extends AbstractCanalStoreScavenge implements
  2. CanalEventStore<Event>, CanalStoreScavenge {
  3. private static final long INIT_SQEUENCE = -1;
  4. private int               bufferSize    = 16 * 1024;
  5. // memsize的单位,默认为1kb大小
  6. private int               bufferMemUnit = 1024;
  7. private int               indexMask;
  8. private Event[]           entries;
  9. // 记录下put/get/ack操作的三个下标,初始值都是-1
  10. // 代表当前put操作最后一次写操作发生的位置
  11. private AtomicLong   putSequence   = new AtomicLong(INIT_SQEUENCE);
  12. // 代表当前get操作读取的最后一条的位置
  13. private AtomicLong   getSequence   = new AtomicLong(INIT_SQEUENCE);
  14. // 代表当前ack操作的最后一条的位置
  15. private AtomicLong   ackSequence   = new AtomicLong(INIT_SQEUENCE);
  16. // 记录下put/get/ack操作的三个memsize大小
  17. private AtomicLong   putMemSize    = new AtomicLong(0);
  18. private AtomicLong   getMemSize    = new AtomicLong(0);
  19. private AtomicLong   ackMemSize    = new AtomicLong(0);
  20. // 阻塞put/get操作控制信号
  21. private ReentrantLock     lock          = new ReentrantLock();
  22. private Condition    notFull       = lock.newCondition();
  23. private Condition    notEmpty      = lock.newCondition();
  24. // 默认为内存大小模式
  25. private BatchMode    batchMode     = BatchMode.ITEMSIZE;
  26. private boolean     ddlIsolation  = false;
  27. ...
  28. }

属性说明:

bufferSize、bufferMemUnit、batchMode、ddlIsolation、putSequence、getSequence、ackSequence:

这几个属性前面已经介绍过,这里不再赘述。

entries:

类型为Event[]数组,环形队列底层基于的Event[]数组,队列的大小就是bufferSize。关于如何使用数组来实现环形队列,可参考笔者的另一篇文章http://www.tianshouzhi.com/api/tutorials/basicalgorithm/43

indexMask

用于对putSequence、getSequence、ackSequence进行取余操作,前面已经介绍过canal通过位操作进行取余,其值为bufferSize-1 ,参见下文的start方法

putMemSize、getMemSize、ackMemSize:

分别用于记录put/get/ack操作的event占用内存的累加值,都是从0开始计算。例如每put一个event,putMemSize就要增加这个event占用的内存大小;get和ack操作也是类似。这三个变量,都是在batchMode指定为MEMSIZE的情况下,才会发生作用。

因为都是累加值,所以我们需要进行一些运算,才能得有有用的信息,如:

计算出当前环形队列当前占用的内存大小

  1. 环形队列当前占用的内存大小 = putMemSize - ackMemSize

前面我们提到,batchMode为MEMSIZE时,需要限制环形队列中event占用的总内存,事实上在执行put操作前,就是通过这种方式计算出来当前大小,然后我们限制的bufferSize * bufferMemUnit大小进行比较。

计算尚未被获取的事件占用的内存大小

  1. 尚未被获取的事件占用的内存大小 = putMemSize - getMemSize

batchMode除了对PUT操作有限制,对Get操作也有影响。Get操作可以指定一个batchSize,用于指定批量获取的大小。当batchMode为MEMSIZE时,其含义就在不再是记录数,而是要获取到总共占用 batchSize * bufferMemUnit 内存大小的事件数量。

lock、notFull、notEmpty:

阻塞put/get操作控制信号。notFull用于控制put操作,只有队列没满的情况下才能put。notEmpty控制get操作,只有队列不为空的情况下,才能get。put操作和get操作共用一把锁(lock)。

3.2 启动和停止方法

MemoryEventStoreWithBuffer实现了CanalLifeCycle接口,因此实现了其定义的start、stop方法

start启动方法

start方法主要是初始化MemoryEventStoreWithBuffer内部的环形队列,其实就是初始化一下Event[]数组。

  1. public void start() throws CanalStoreException {
  2. super.start();
  3. if (Integer.bitCount(bufferSize) != 1) {
  4. throw new IllegalArgumentException("bufferSize must be a power of 2");
  5. }
  6. indexMask = bufferSize - 1;//初始化indexMask,前面已经介绍过,用于通过位操作进行取余
  7. entries = new Event[bufferSize];//创建循环队列基于的底层数组,大小为bufferSize
  8. }

stop停止方法

stop方法作用是停止,在停止时会清空所有缓存的数据,将维护的相关状态变量设置为初始值。

MemoryEventStoreWithBuffer#stop

  1. public void stop() throws CanalStoreException {
  2. super.stop();
  3. //清空所有缓存的数据,将维护的相关状态变量设置为初始值
  4. cleanAll();
  5. }

在停止时,通过调用cleanAll方法清空所有缓存的数据。

cleanAll方法是在CanalStoreScavenge接口中定义的,在MemoryEventStoreWithBuffer中进行了实现, 此外这个接口还定义了另外一个方法cleanUtil,在执行ack操作时会被调用,我们将在介绍ack方法时进行讲解。

MemoryEventStoreWithBuffer#cleanAll

  1. public void cleanAll() throws CanalStoreException {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. //将Put/Get/Ack三个操作的位置都重置为初始状态-1
  6. putSequence.set(INIT_SQEUENCE);
  7. getSequence.set(INIT_SQEUENCE);
  8. ackSequence.set(INIT_SQEUENCE);
  9. //将Put/Get/Ack三个操作的memSize都重置为0
  10. putMemSize.set(0);
  11. getMemSize.set(0);
  12. ackMemSize.set(0);
  13. //将底层Event[]数组置为null,相当于清空所有数据
  14. entries = null;
  15. } finally {
  16. lock.unlock();
  17. }
  18. }

4.2 Put操作

前面分析CanalEventStore接口中,我们看到总共有6个put方法,可以分为3类:

  • 不带timeout超时参数的put方法,会一直进行阻塞,直到有足够的空间可以放入。

  • 带timeout参数超时参数的put方法,如果超过指定时间还未put成功,会抛出InterruptedException。

  • tryPut方法每次只是尝试放入数据,立即返回true或者false,不会阻塞。

事实上,这些方法只是超时机制不同,底层都是通过调用doPut方法来完成真正的数据放入。因此在后面的分析中,笔者只选择其中一种进行讲解。

所有的put操作,在放入数据之前,都需要进行一些前置检查工作,主要检查2点:

1、检查是否足够的slot

默认的bufferSize设置大小为16384,即有16384个slot,每个slot可以存储一个event,因此canal默认最多缓存16384个event。从来另一个角度出发,这意味着putSequence最多比ackSequence可以大16384,不能超过这个值。如果超过了,就意味着尚未没有被消费的数据被覆盖了,相当于丢失了数据。因此,如果Put操作满足以下条件时,是不能新加入数据的

  1. (putSequence + need_put_events_size)- ackSequence > bufferSize

"putSequence + need_put_events_size"的结果为添加数据后的putSequence的最终位置值,要把这个作为预判断条件,其减去ackSequence,如果大于bufferSize,则不能插入数据。需要等待有足够的空间,或者抛出异常。

2、检测是否超出了内存限制

前面我们已经看到了,为了控制队列中event占用的总内存大小,可以指定batchMode为MEMSIZE。在这种情况下,buffer.size  * buffer.memunit(默认为16M)就表示环形队列存储的event总共可以占用的内存大小。因此当出现以下情况下, 不能加入新的event:

  1. (putMemSize - ackMemSize) > buffer.size  * buffer.memunit

关于putMemSize和ackMemSize前面已经介绍过,二者的差值,实际上就是"队列当前包含的event占用的总内存”。

下面我们选择可以指定timeout超时时间的put方法进行讲解,如下:

  1. public boolean put(List<Event> data, long timeout, TimeUnit unit) throws InterruptedException,
  2. CanalStoreException {
  3. //1 如果需要插入的List为空,直接返回true
  4. if (data == null || data.isEmpty()) {
  5. return true;
  6. }
  7. //2 获得超时时间,并通过加锁进行put操作
  8. long nanos = unit.toNanos(timeout);
  9. final ReentrantLock lock = this.lock;
  10. lock.lockInterruptibly();
  11. try {
  12. for (;;) {//这是一个死循环,执行到下面任意一个return或者抛出异常是时才会停止
  13. //3 检查是否满足插入条件,如果满足,进入到3.1,否则进入到3.2
  14. if (checkFreeSlotAt(putSequence.get() + data.size())) {
  15. //3.1 如果满足条件,调用doPut方法进行真正的插入
  16. doPut(data);
  17. return true;
  18. }
  19. //3.2 判断是否已经超时,如果超时,则不执行插入操作,直接返回false
  20. if (nanos <= 0) {
  21. return false;
  22. }
  23. //3.3 如果还没有超时,调用notFull.awaitNanos进行等待,需要其他线程调用notFull.signal()方法唤醒。
  24. //唤醒是在ack操作中进行的,ack操作会删除已经消费成功的event,此时队列有了空间,因此可以唤醒,详见ack方法分析
  25. //当被唤醒后,因为这是一个死循环,所以循环中的代码会重复执行。当插入条件满足时,调用doPut方法插入,然后返回
  26. try {
  27. nanos = notFull.awaitNanos(nanos);
  28. //3.4 如果一直等待到超时,都没有可用空间可以插入,notFull.awaitNanos会抛出InterruptedException
  29. } catch (InterruptedException ie) {
  30. notFull.signal(); //3.5 超时之后,唤醒一个其他执行put操作且未被中断的线程(不明白是为了干啥)
  31. throw ie;
  32. }
  33. }
  34. } finally {
  35. lock.unlock();
  36. }
  37. }

上述方法的第3步,通过调用checkFreeSlotAt方法来执行插入数据前的检查工作,所做的事情就是我们前面提到的2点:1、检查是否足够的slot 2、检测是否超出了内存限制,源码如下所示:

MemoryEventStoreWithBuffer#checkFreeSlotAt

  1. /**查询是否有空位*/
  2. private boolean checkFreeSlotAt(final long sequence) {
  3. //1、检查是否足够的slot。注意方法参数传入的sequence值是:当前putSequence值 + 新插入的event的记录数。
  4. //按照前面的说明,其减去bufferSize不能大于ack位置,或者换一种说法,减去bufferSize不能大于ack位置。
  5. //1.1 首先用sequence值减去bufferSize
  6. final long wrapPoint = sequence - bufferSize;
  7. //1.2 获取get位置ack位置的较小值,事实上,ack位置总是应该小于等于get位置,因此这里总是应该返回的是ack位置。
  8. final long minPoint = getMinimumGetOrAck();
  9. //1.3 将1.1 与1.2步得到的值进行比较,如果前者大,说明二者差值已经超过了bufferSize,不能插入数据,返回false
  10. if (wrapPoint > minPoint) { // 刚好追上一轮
  11. return false;
  12. } else {
  13. //2 如果batchMode是MEMSIZE,继续检查是否超出了内存限制。
  14. if (batchMode.isMemSize()) {
  15. //2.1 使用putMemSize值减去ackMemSize值,得到当前保存的event事件占用的总内存
  16. final long memsize = putMemSize.get() - ackMemSize.get();
  17. //2.2 如果没有超出bufferSize * bufferMemUnit内存限制,返回true,否则返回false
  18. if (memsize < bufferSize * bufferMemUnit) {
  19. return true;
  20. } else {
  21. return false;
  22. }
  23. } else {
  24. //3 如果batchMode不是MEMSIZE,说明只限制记录数,则直接返回true
  25. return true;
  26. }
  27. }
  28. }

getMinimumGetOrAck方法用于返回getSequence和ackSequence二者的较小值,源码如下所示:

MemoryEventStoreWithBuffer#getMinimumGetOrAck

  1. private long getMinimumGetOrAck() {
  2. long get = getSequence.get();
  3. long ack = ackSequence.get();
  4. return ack <= get ? ack : get;
  5. }

如前所述,ackSequence总是应该小于等于getSequence,因此这里判断应该是没有必要的,笔者已经给官方提了issue,也得到了确认,参见:https://github.com/alibaba/canal/issues/966

当checkFreeSlotAt方法检验通过后,最终调用的是doPut方法进行插入。doPut方法主要有4个步骤:

1、将新插入的event数据赋值到Event[]数组的正确位置上,就算完成了插入

2、当新插入的event记录数累加到putSequence上

3、累加新插入的event的大小到putMemSize上

4、调用notEmpty.signal()方法,通知队列中有数据了,如果之前有client获取数据处于阻塞状态,将会被唤醒

MemoryEventStoreWithBuffer#doPut

  1. /*** 执行具体的put操作*/
  2. private void doPut(List<Event> data) {
  3. //1 将新插入的event数据赋值到Event[]数组的正确位置上
  4. //1.1 获得putSequence的当前值current,和插入数据后的putSequence结束值end
  5. long current = putSequence.get();
  6. long end = current + data.size();
  7. //1.2 循环需要插入的数据,从current位置开始,到end位置结束
  8. for (long next = current + 1; next <= end; next++) {
  9. //1.3 通过getIndex方法对next变量转换成正确的位置,设置到Event[]数组中
  10. //需要转换的原因在于,这里的Event[]数组是环形队列的底层实现,其大小为bufferSize值,默认为16384。
  11. //运行一段时间后,接收到的binlog数量肯定会超过16384,每接受到一个event,putSequence+1,因此最终必然超过这个值。
  12. //而next变量是比当前putSequence值要大的,因此必须进行转换,否则会数组越界,转换工作就是在getIndex方法中进行的。
  13. entries[getIndex(next)] = data.get((int) (next - current - 1));
  14. }
  15. //2 直接设置putSequence为end值,相当于完成event记录数的累加
  16. putSequence.set(end);
  17. //3 累加新插入的event的大小到putMemSize上
  18. if (batchMode.isMemSize()) {
  19. //用于记录本次插入的event记录的大小
  20. long size = 0;
  21. //循环每一个event
  22. for (Event event : data) {
  23. //通过calculateSize方法计算每个event的大小,并累加到size变量上
  24. size += calculateSize(event);
  25. }
  26. //将size变量的值,添加到当前putMemSize
  27. putMemSize.getAndAdd(size);
  28. }
  29. // 4 调用notEmpty.signal()方法,通知队列中有数据了,如果之前有client获取数据处于阻塞状态,将会被唤醒
  30. notEmpty.signal();
  31. }

上述代码中,通过getIndex方法方法来进行位置转换,其内部通过位运算来快速取余数,不再赘述

MemoryEventStoreWithBuffer#getIndex

  1. private int getIndex(long sequcnce) {
  2. return (int) sequcnce & indexMask; //bufferSize-1
  3. }

对于batchMode是MEMSIZE的情况下, 还会通过calculateSize方法计算每个event占用的内存大小,累加到putMemSize上。

MemoryEventStoreWithBuffer#calculateSize

  1. private long calculateSize(Event event) {
  2. // 直接返回binlog中的事件大小
  3. return event.getEntry().getHeader().getEventLength();
  4. }

其原理在于,mysql的binlog的event header中,都有一个event_length表示这个event占用的字节数。不熟悉mysql binlog event结构的读者可参考:https://dev.mysql.com/doc/internals/en/event-structure.html

parser模块将二进制形式binlog event解析后,这个event_length字段的值也被解析出来了,转换成Event对象后,在存储到store模块时,就可以根据其值判断占用内存大小。

需要注意的是,这个计算并不精确。原始的event_length表示的是event是二进制字节流时的字节数,在转换成java对象后,基本上都会变大。如何获取java对象的真实大小,可参考这个博客:https://www.cnblogs.com/Kidezyq/p/8030098.html

4.3 Get操作

Put操作是canal parser模块解析binlog事件,并经过sink模块过滤后,放入到store模块中,也就是说Put操作实际上是canal内部调用。 Get操作(以及ack、rollback)则不同,其是由client发起的网络请求,server端通过对请求参数进行解析,最终调用CanalEventStore模块中定义的对应方法。

Get操作用于获取指定batchSize大小的Events。提供了3个方法:

  1. // 尝试获取,如果获取不到立即返回
  2. public Events<Event> tryGet(Position start, int batchSize) throws CanalStoreException
  3. // 获取指定大小的数据,阻塞等待其操作完成
  4. public Events<Event> get(Position start, int batchSize) throws InterruptedException, CanalStoreException
  5. // 获取指定大小的数据,阻塞等待其操作完成或者超时,如果超时了,有多少,返回多少
  6. public Events<Event> get(Position start, int batchSize, long timeout, TimeUnit unit)
  7. throws InterruptedException,CanalStoreException

其中:

  • start参数:其类型为Posisiton,表示从哪个位置开始获取

  • batchSize参数:表示批量获取的大小

  • timeout和uint参数:超时参数配置

与Put操作类似,MemoryEventStoreWithBuffer在实现这三个方法时,真正的获取操作都是在doGet方法中进行的。这里我们依然只选择其中一种进行完整的讲解:

  1. public Events<Event> get(Position start, int batchSize, long timeout, TimeUnit unit)
  2. throws InterruptedException,CanalStoreException {
  3. long nanos = unit.toNanos(timeout);
  4. final ReentrantLock lock = this.lock;
  5. lock.lockInterruptibly();
  6. try {
  7. for (;;) {
  8. if (checkUnGetSlotAt((LogPosition) start, batchSize)) {
  9. return doGet(start, batchSize);
  10. }
  11. if (nanos <= 0) {
  12. // 如果时间到了,有多少取多少
  13. return doGet(start, batchSize);
  14. }
  15. try {
  16. nanos = notEmpty.awaitNanos(nanos);
  17. } catch (InterruptedException ie) {
  18. notEmpty.signal(); // propagate to non-interrupted thread
  19. throw ie;
  20. }
  21. }
  22. } finally {
  23. lock.unlock();
  24. }
  25. }

可以看到,get方法的实现逻辑与put方法整体上是类似的,不再赘述。这里我们直接关注checkUnGetSlotAtdoGet方法。

checkUnGetSlotAt方法,用于检查是否有足够的event可供获取,根据batchMode的不同,有着不同的判断逻辑

  • 如果batchMode为ITEMSIZE,则表示只要有有满足batchSize数量的记录数即可,即putSequence - getSequence >= batchSize

  • 如果batchMode为MEMSIZE,此时batchSize不再表示记录数,而是bufferMemUnit的个数,也就是说,获取到的event列表占用的总内存要达到batchSize * bufferMemUnit,即putMemSize-getMemSize >= batchSize * bufferMemUnit

MemoryEventStoreWithBuffer#checkUnGetSlotAt

  1. private boolean checkUnGetSlotAt(LogPosition startPosition, int batchSize) {
  2. //1 如果batchMode为ITEMSIZE
  3. if (batchMode.isItemSize()) {
  4. long current = getSequence.get();
  5. long maxAbleSequence = putSequence.get();
  6. long next = current;
  7. //1.1 第一次订阅之后,需要包含一下start位置,防止丢失第一条记录。
  8. if (startPosition == null || !startPosition.getPostion().isIncluded()) {
  9. next = next + 1;
  10. }
  11. //1.2 理论上只需要满足条件:putSequence - getSequence >= batchSize
  12. //1.2.1 先通过current < maxAbleSequence进行一下简单判断,如果不满足,可以直接返回false了
  13. //1.2.2 如果1.2.1满足,再通过putSequence - getSequence >= batchSize判断是否有足够的数据
  14. if (current < maxAbleSequence && next + batchSize - 1 <= maxAbleSequence) {
  15. return true;
  16. } else {
  17. return false;
  18. }
  19. //2 如果batchMode为MEMSIZE
  20. } else {
  21. long currentSize = getMemSize.get();
  22. long maxAbleSize = putMemSize.get();
  23. //2.1 需要满足条件 putMemSize-getMemSize >= batchSize * bufferMemUnit
  24. if (maxAbleSize - currentSize >= batchSize * bufferMemUnit) {
  25. return true;
  26. } else {
  27. return false;
  28. }
  29. }
  30. }

关于1.1步的描述"第一次订阅之后,需要包含一下start位置,防止丢失第一条记录”,这里进行一下特殊说明。首先要明确checkUnGetSlotAt方法的startPosition参数到底是从哪里传递过来的。

当一个client在获取数据时,CanalServerWithEmbedded的getWithoutAck/或get方法会被调用。其内部首先通过CanalMetaManager查找client的消费位置信息,由于是第一次,肯定没有记录,因此返回null,此时会调用CanalEventStore的getFirstPosition()方法,尝试把第一条数据作为消费的开始。而此时CanalEventStore中可能有数据,也可能没有数据。在没有数据的情况下,依然返回null;在有数据的情况下,把第一个Event的位置作为消费开始位置。那么显然,传入checkUnGetSlotAt方法的startPosition参数可能是null,也可能不是null。所以有了以下处理逻辑:

  1. if (startPosition == null || !startPosition.getPostion().isIncluded()) {
  2. next = next + 1;
  3. }

如果不是null的情况下,尽管把第一个event当做开始位置,但是因为这个event毕竟还没有消费,所以在消费的时候我们必须也将其包含进去。之所以要+1,因为是第一次获取,getSequence的值肯定还是初始值-1,所以要+1变成0之后才是队列的第一个event位置。关于CanalEventStore的getFirstPosition()方法,我们将在最后分析。

当通过checkUnGetSlotAt的检查条件后,通过doGet方法进行真正的数据获取操作,获取主要分为5个步骤:

1、确定从哪个位置开始获取数据

2、根据batchMode是MEMSIZE还是ITEMSIZE,通过不同的方式来获取数据

3、设置PositionRange,表示获取到的event列表开始和结束位置

4、设置ack点

5、累加getSequence,getMemSize值

MemoryEventStoreWithBuffer#doGet

  1. private Events<Event> doGet(Position start, int batchSize) throws CanalStoreException {
  2. LogPosition startPosition = (LogPosition) start;
  3. //1 确定从哪个位置开始获取数据
  4. //获得当前的get位置
  5. long current = getSequence.get();
  6. //获得当前的put位置
  7. long maxAbleSequence = putSequence.get();
  8. //要获取的第一个Event的位置,一开始等于当前get位置
  9. long next = current;
  10. //要获取的最后一个event的位置,一开始也是当前get位置,每获取一个event,end值加1,最大为current+batchSize
  11. //因为可能进行ddl隔离,因此可能没有获取到batchSize个event就返回了,此时end值就会小于current+batchSize
  12. long end = current;
  13. // 如果startPosition为null,说明是第一次订阅,默认+1处理,因为getSequence的值是从-1开始的
  14. // 如果tartPosition不为null,需要包含一下start位置,防止丢失第一条记录
  15. if (startPosition == null || !startPosition.getPostion().isIncluded()) {
  16. next = next + 1;
  17. }
  18. // 如果没有数据,直接返回一个空列表
  19. if (current >= maxAbleSequence) {
  20. return new Events<Event>();
  21. }
  22. //2 如果有数据,根据batchMode是ITEMSIZE或MEMSIZE选择不同的处理方式
  23. Events<Event> result = new Events<Event>();
  24. //维护要返回的Event列表
  25. List<Event> entrys = result.getEvents();
  26. long memsize = 0;
  27. //2.1 如果batchMode是ITEMSIZE
  28. if (batchMode.isItemSize()) {
  29. end = (next + batchSize - 1) < maxAbleSequence ? (next + batchSize - 1) : maxAbleSequence;
  30. //2.1.1 循环从开始位置(next)到结束位置(end),每次循环next+1
  31. for (; next <= end; next++) {
  32. //2.1.2 获取指定位置上的事件
  33. Event event = entries[getIndex(next)];
  34. //2.1.3 果是当前事件是DDL事件,且开启了ddl隔离,本次事件处理完后,即结束循环(if语句最后是一行是break)
  35. if (ddlIsolation && isDdl(event.getEntry().getHeader().getEventType())) {
  36. // 2.1.4 因为ddl事件需要单独返回,因此需要判断entrys中是否应添加了其他事件
  37. if (entrys.size() == 0) {//如果entrys中尚未添加任何其他event
  38. entrys.add(event);//加入当前的DDL事件
  39. end = next; // 更新end为当前值
  40. } else {
  41. //如果已经添加了其他事件 如果之前已经有DML事件,直接返回了,因为不包含当前next这记录,需要回退一个位置
  42. end = next - 1; // next-1一定大于current,不需要判断
  43. }
  44. break;
  45. } else {//如果没有开启DDL隔离,直接将事件加入到entrys中
  46. entrys.add(event);
  47. }
  48. }
  49. //2.2 如果batchMode是MEMSIZE
  50. } else {
  51. //2.2.1 计算本次要获取的event占用最大字节数
  52. long maxMemSize = batchSize * bufferMemUnit;
  53. //2.2.2 memsize从0开始,当memsize小于maxMemSize且next未超过maxAbleSequence时,可以进行循环
  54. for (; memsize <= maxMemSize && next <= maxAbleSequence; next++) {
  55. //2.2.3 获取指定位置上的Event
  56. Event event = entries[getIndex(next)];
  57. //2.2.4 果是当前事件是DDL事件,且开启了ddl隔离,本次事件处理完后,即结束循环(if语句最后是一行是break)
  58. if (ddlIsolation && isDdl(event.getEntry().getHeader().getEventType())) {
  59. // 如果是ddl隔离,直接返回
  60. if (entrys.size() == 0) {
  61. entrys.add(event);// 如果没有DML事件,加入当前的DDL事件
  62. end = next; // 更新end为当前
  63. } else {
  64. // 如果之前已经有DML事件,直接返回了,因为不包含当前next这记录,需要回退一个位置
  65. end = next - 1; // next-1一定大于current,不需要判断
  66. }
  67. break;
  68. } else {
  69. //如果没有开启DDL隔离,直接将事件加入到entrys中
  70. entrys.add(event);
  71. //并将当前添加的event占用字节数累加到memsize变量上
  72. memsize += calculateSize(event);
  73. end = next;// 记录end位点
  74. }
  75. }
  76. }
  77. //3 构造PositionRange,表示本次获取的Event的开始和结束位置
  78. PositionRange<LogPosition> range = new PositionRange<LogPosition>();
  79. result.setPositionRange(range);
  80. //3.1 把entrys列表中的第一个event的位置,当做PositionRange的开始位置
  81. range.setStart(CanalEventUtils.createPosition(entrys.get(0)));
  82. //3.2 把entrys列表中的最后一个event的位置,当做PositionRange的结束位置
  83. range.setEnd(CanalEventUtils.createPosition(entrys.get(result.getEvents().size() - 1)));
  84. //4 记录一下是否存在可以被ack的点,逆序迭代获取到的Event列表
  85. for (int i = entrys.size() - 1; i >= 0; i--) {
  86. Event event = entrys.get(i);
  87. //4.1.1 如果是事务开始/事务结束/或者dll事件,
  88. if (CanalEntry.EntryType.TRANSACTIONBEGIN == event.getEntry().getEntryType()
  89. || CanalEntry.EntryType.TRANSACTIONEND == event.getEntry().getEntryType()
  90. || isDdl(event.getEntry().getHeader().getEventType())) {
  91. // 4.1.2 将其设置为可被ack的点,并跳出循环
  92. range.setAck(CanalEventUtils.createPosition(event));
  93. break;
  94. }
  95. //4.1.3 如果没有这三种类型事件,意味着没有可被ack的点
  96. }
  97. //5 累加getMemSize值,getMemSize值
  98. //5.1 通过AtomLong的compareAndSet尝试增加getSequence值
  99. if (getSequence.compareAndSet(current, end)) {//如果成功,累加getMemSize
  100. getMemSize.addAndGet(memsize);
  101. //如果之前有put操作因为队列满了而被阻塞,这里发送信号,通知队列已经有空位置,下面还要进行说明
  102. notFull.signal();
  103. return result;
  104. } else {//如果失败,直接返回空事件列表
  105. return new Events<Event>();
  106. }
  107. }

补充说明:

1 Get数据时,会通过isDdl方法判断event是否是ddl类型。

MemoryEventStoreWithBuffer#isDdl

  1. private boolean isDdl(EventType type) {
  2. return type == EventType.ALTER || type == EventType.CREATE || type == EventType.ERASE
  3. || type == EventType.RENAME || type == EventType.TRUNCATE || type == EventType.CINDEX
  4. || type == EventType.DINDEX;
  5. }

这里的EventType是在protocol模块中定义的,并非mysql binlog event结构中的event type。在原始的mysql binlog event类型中,有一个QueryEvent,里面记录的是执行的sql语句,canal通过对这个sql语句进行正则表达式匹配,判断出这个event是否是DDL语句(参见SimpleDdlParser#parse方法)。

2 获取到event列表之后,会构造一个PostionRange对象。

通过CanalEventUtils.createPosition方法计算出第一、最后一个event的位置,作为PostionRange的开始和结束。

事实上,parser模块解析后,已经将位置信息:binlog文件,position封装到了Event中,createPosition方法只是将这些信息提取出来。

CanalEventUtils#createPosition

  1. public static LogPosition createPosition(Event event) {
  2. //=============创建一个EntryPosition实例,提取event中的位置信息============
  3. EntryPosition position = new EntryPosition();
  4. //event所在的binlog文件
  5. position.setJournalName(event.getEntry().getHeader().getLogfileName());
  6. //event锁在binlog文件中的位置
  7. position.setPosition(event.getEntry().getHeader().getLogfileOffset());
  8. //event的创建时间
  9. position.setTimestamp(event.getEntry().getHeader().getExecuteTime());
  10. //event是mysql主从集群哪一个实例上生成的,一般都是主库,如果从库没有配置read-only,那么serverId也可能是从库
  11. position.setServerId(event.getEntry().getHeader().getServerId());
  12. //===========将EntryPosition实例封装到一个LogPosition对象中===============
  13. LogPosition logPosition = new LogPosition();
  14. logPosition.setPostion(position);
  15. //LogIdentity中包含了这个event来源的mysql实力的ip地址信息
  16. logPosition.setIdentity(event.getLogIdentity());
  17. return logPosition;
  18. }

3 获取到Event列表后,会从中逆序寻找第一个类型为"事务开始/事务结束/DDL"的Event,将其位置作为PostionRange的可ack位置。

mysql原生的binlog事件中,总是以一个内容”BEGIN”的QueryEvent作为事务开始,以XidEvent事件表示事务结束。即使我们没有显式的开启事务,对于单独的一个更新语句(如Insert、update、delete),mysql也会默认开启事务。而canal将其转换成更容易理解的自定义EventType类型:TRANSACTIONBEGIN、TRANSACTIONEND。

而将这些事件作为ack点,主要是为了保证事务的完整性。例如client一次拉取了10个binlog event,前5个构成一个事务,后5个还不足以构成一个完整事务。在ack后,如果这个client停止了,也就是说下一个事务还没有被完整处理完。尽管之前ack的是10条数据,但是client重新启动后,将从第6个event开始消费,而不是从第11个event开始消费,因为第6个event是下一个事务的开始。

具体逻辑在于,canal server在接受到client ack后,CanalServerWithEmbedded#ack方法会执行。其内部首先根据ack的batchId找到对应的PositionRange,再找出其中的ack点,通过CanalMetaManager将这个位置记录下来。之后client重启后,再把这个位置信息取出来,从这个位置开始消费。

也就是说,ack位置实际上提供给CanalMetaManager使用的。而对于MemoryEventStoreWithBuffer本身而言,也需要进行ack,用于将已经消费的数据从队列中清除,从而腾出更多的空间存放新的数据。

4.4 ack操作

相对于get操作和put操作,ack操作没有重载,只有一个ack方法,用于清空指定position之前的数据,如下:

MemoryEventStoreWithBuffer#ack

  1. public void ack(Position position) throws CanalStoreException {
  2. cleanUntil(position);
  3. }

CanalStoreScavenge接口定义了2个方法:cleanAll和cleanUntil。前面我们已经看到了在stop时,cleanAll方法会被执行。而每次ack时,cleanUntil方法会被执行,这个方法实现如下所示:

MemoryEventStoreWithBuffer#cleanUntil

  1. // postion表示要ack的配置
  2. public void cleanUntil(Position position) throws CanalStoreException {
  3. final ReentrantLock lock = this.lock;
  4. lock.lock();
  5. try {
  6. //获得当前ack值
  7. long sequence = ackSequence.get();
  8. //获得当前get值
  9. long maxSequence = getSequence.get();
  10. boolean hasMatch = false;
  11. long memsize = 0;
  12. //迭代所有未被ack的event,从中找出与需要ack的position相同位置的event,清空这个event之前的所有数据。
  13. //一旦找到这个event,循环结束。
  14. for (long next = sequence + 1; next <= maxSequence; next++) {
  15. Event event = entries[getIndex(next)];//获得要ack的event
  16. memsize += calculateSize(event);//计算当前要ack的event占用字节数
  17. boolean match = CanalEventUtils.checkPosition(event, (LogPosition) position);
  18. if (match) {// 找到对应的position,更新ack seq
  19. hasMatch = true;
  20. if (batchMode.isMemSize()) {//如果batchMode是MEMSIZE
  21. ackMemSize.addAndGet(memsize);//累加ackMemSize
  22. // 尝试清空buffer中的内存,将ack之前的内存全部释放掉
  23. for (long index = sequence + 1; index < next; index++) {
  24. entries[getIndex(index)] = null;// 设置为null
  25. }
  26. }
  27. //累加ack值
  28. //官方注释说,采用compareAndSet,是为了避免并发ack。我觉得根本不会并发ack,因为都加锁了
  29. if (ackSequence.compareAndSet(sequence, next)) {
  30. notFull.signal();//如果之前存在put操作因为队列满了而被阻塞,通知其队列有了新空间
  31. return;
  32. }
  33. }
  34. }
  35. if (!hasMatch) {// 找不到对应需要ack的position
  36. throw new CanalStoreException("no match ack position" + position.toString());
  37. }
  38. } finally {
  39. lock.unlock();
  40. }
  41. }

在匹配尚未ack的Event,是否有匹配的位置时,调用了CanalEventUtils#checkPosition方法。其内部:

  • 首先比较Event的生成时间

  • 接着,如果位置信息的binlog文件名或者信息不为空的话(通常不为空),则会进行精确匹配

CanalEventUtils#checkPosition

  1. /**
  2. * 判断当前的entry和position是否相同
  3. */
  4. public static boolean checkPosition(Event event, LogPosition logPosition) {
  5. EntryPosition position = logPosition.getPostion();
  6. CanalEntry.Entry entry = event.getEntry();
  7. //匹配时间
  8. boolean result = position.getTimestamp().equals(entry.getHeader().getExecuteTime());
  9. //判断是否需要根据:binlog文件+position进行比较
  10. boolean exactely = (StringUtils.isBlank(position.getJournalName()) && position.getPosition() == null);
  11. if (!exactely) {// 精确匹配
  12. result &= StringUtils.equals(entry.getHeader().getLogfileName(), position.getJournalName());
  13. result &= position.getPosition().equals(entry.getHeader().getLogfileOffset());
  14. }
  15. return result;
  16. }

4.5 rollback操作

相对于put/get/ack操作,rollback操作简单了很多。所谓rollback,就是client已经get到的数据,没能消费成功,因此需要进行回滚。回滚操作特别简单,只需要将getSequence的位置重置为ackSequence,将getMemSize设置为ackMemSize即可。

  1. public void rollback() throws CanalStoreException {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. getSequence.set(ackSequence.get());
  6. getMemSize.set(ackMemSize.get());
  7. } finally {
  8. lock.unlock();
  9. }
  10. }

4.6 其他方法

除了上述提到的所有方法外,MemoryEventStoreWithBuffer还提供了getFirstPosition()getLatestPosition()方法,分别用于获取当前队列中的第一个和最后一个Event的位置信息。前面已经提到,在CanalServerWithEmbedded中会使用getFirstPosition()方法来获取CanalEventStore中存储的第一个Event的位置,而getLatestPosition()只是在一些单元测试中使用到,因此在这里我们只分析getFirstPosition()方法。

第一条数据通过ackSequence当前值对应的Event来确定,因为更早的Event在ack后都已经被删除了。相关源码如下:

MemoryEventStoreWithBuffer#getFirstPosition

  1. //获取第一条数据的position,如果没有数据返回为null
  2. public LogPosition getFirstPosition() throws CanalStoreException {
  3. final ReentrantLock lock = this.lock;
  4. lock.lock();
  5. try {
  6. long firstSeqeuence = ackSequence.get();
  7. //1 没有ack过数据,且队列中有数据
  8. if (firstSeqeuence == INIT_SQEUENCE && firstSeqeuence < putSequence.get()) {
  9. //没有ack过数据,那么ack为初始值-1,又因为队列中有数据,因此ack+1,即返回队列中第一条数据的位置
  10. Event event = entries[getIndex(firstSeqeuence + 1)];
  11. return CanalEventUtils.createPosition(event, false);
  12. //2 已经ack过数据,但是未追上put操作
  13. } else if (firstSeqeuence > INIT_SQEUENCE && firstSeqeuence < putSequence.get()) {
  14. //返回最后一次ack的位置数据 + 1
  15. Event event = entries[getIndex(firstSeqeuence + 1)];
  16. return CanalEventUtils.createPosition(event, true);
  17. //3 已经ack过数据,且已经追上put操作,说明队列中所有数据都被消费完了
  18. } else if (firstSeqeuence > INIT_SQEUENCE && firstSeqeuence == putSequence.get()) {
  19. // 最后一次ack的位置数据,和last为同一条
  20. Event event = entries[getIndex(firstSeqeuence)];
  21. return CanalEventUtils.createPosition(event, false);
  22. //4 没有任何数据,返回null
  23. } else {
  24. return null;
  25. }
  26. } finally {
  27. lock.unlock();
  28. }
  29. }

代码逻辑很简单,唯一需要关注的是,通过CanalEventUtils#createPosition(Event, boolean)方法来计算第一个Event的位置,返回的是一个LogPosition对象。其中boolean参数用LogPosition内部维护的EntryPosition的included属性赋值。在前面get方法源码分析时,我们已经看到,当included值为false时,会把当前get位置+1,然后开始获取Event;当为true时,则直接从当前get位置开始获取数据。

6.0 filter模块

 2018-11-03 01:16:48  9,173 1

1 Filter模块简介

filter模块用于对binlog进行过滤。在实际开发中,一个mysql实例中可能会有多个库,每个库里面又会有多个表,可能我们只是想订阅某个库中的部分表,这个时候就需要进行过滤。也就是说,parser模块解析出来binlog之后,会进行一次过滤之后,才会存储到store模块中。

过滤规则的配置既可以在canal服务端进行,也可以在客户端进行。

1.1 服务端配置

我们在配置一个canal instance时,在instance.properties中有以下两个配置项:

其中:

canal.instance.filter.regex用于配置白名单,也就是我们希望订阅哪些库,哪些表,默认值为.*\\..*,也就是订阅所有库,所有表。

canal.instance.filter.black.regex用于配置黑名单,也就是我们不希望订阅哪些库,哪些表。没有默认值,也就是默认黑名单为空。

需要注意的是,在过滤的时候,会先根据白名单进行过滤,再根据黑名单过滤。意味着,如果一张表在白名单和黑名单中都出现了,那么这张表最终不会被订阅到,因为白名单通过后,黑名单又将这张表给过滤掉了。

另外一点值得注意的是,过滤规则使用的是perl正则表达式,而不是jdk自带的正则表达式。意味着filter模块引入了其他依赖,来进行匹配。具体来说,filter模块的pom.xml中包含以下两个依赖:

  1. <dependency>
  2. <groupId>com.googlecode.aviator</groupId>
  3. <artifactId>aviator</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>oro</groupId>
  7. <artifactId>oro</artifactId>
  8. </dependency>

其中:

aviator:是一个开源的、高性能、轻量级的 java 语言实现的表达式求值引擎

oro:全称为Jakarta ORO,最全面以及优化得最好的正则表达式API之一,Jakarta-ORO库以前叫做OROMatcher,是由DanielF. Savarese编写,后来捐赠给了apache Jakarta Project。canal的过滤规则就是通过oro中的Perl5Matcher来进行完成的。

显然,对于filter模块的源码解析,实际上主要变成了对aviator、oro的分析。

这一点,我们可以从filter模块核心接口CanalEventFilter的实现类中得到验证。CanalEventFilter接口定义了一个filter方法:

  1. public interface CanalEventFilter<T> {
  2. boolean filter(T event) throws CanalFilterException;
  3. }

目前针对CanalEventFilter提供了3个实现类,都是基于开源的java表达式求值引擎Aviator,如下:

提示:这个3个实现都是以Aviater开头,应该是拼写错误,正确的应该是Aviator。

其中:

  • AviaterELFilter:基于Aviator el表达式的匹配过滤

  • AviaterSimpleFilter:基于Aviator进行tableName简单过滤计算,不支持正则匹配

  • AviaterRegexFilter:基于Aviator进行tableName正则匹配的过滤算法。内部使用到了一个RegexFunction类,这是对Aviator自定义的函数的扩展,内部使用到了oro中的Perl5Matcher来进行正则匹配。

需要注意的是,尽管filter模块提供了3个基于Aviator的过滤器实现,但是实际上使用到的只有AviaterRegexFilter。这一点可以在canal-deploy模块提供的xxx-instance.xml配置文件中得要验证。以default-instance.xml为例,eventParser这个bean包含以下两个属性:

  1. <bean id="eventParser" class="com.alibaba.otter.canal.parse.inbound.mysql.MysqlEventParser">
  2. <!-- ... -->
  3. <!-- 解析过滤处理 -->
  4. <property name="eventFilter">
  5. <bean class="com.alibaba.otter.canal.filter.aviater.AviaterRegexFilter" >
  6. <constructor-arg index="0" value="${canal.instance.filter.regex:.*\..*}" />
  7. </bean>
  8. </property>
  9. <property name="eventBlackFilter">
  10. <bean class="com.alibaba.otter.canal.filter.aviater.AviaterRegexFilter" >
  11. <constructor-arg index="0" value="${canal.instance.filter.black.regex:}" />
  12. <constructor-arg index="1" value="false" />
  13. </bean>
  14. </property>
  15. <!-- ... -->
  16. </bean>

其中:

  • eventFilter属性:使用配置项canal.instance.filter.regex的值进行白名单过滤。

  • eventBlackFilter属性:使用配置项canal.instance.filter.black.regex进行黑名单过滤。

这两个属性的值都是通过一个内部bean的方式进行配置,类型都是AviaterRegexFilter。由于其他两个类型的CanalEventFilter实现在parser模块中并没有使用到,因此后文中,我们也只会对AviaterRegexFilter进行分析。

前面提到,parser模块在过滤的时候,会先根据canal.instance.filter.regex进行白名单过滤,再根据 canal.instance.filter.black.regex进行黑名单过滤。到这里,实际上就是先通过eventFilter进行摆明但过滤,通过eventBlackFilter进行黑名单过滤。

parser模块实际上会将eventFilter、eventBlackFilter设置到一个LogEventConvert对象中,这个对象有2个方法:parseQueryEvent和parseRowsEvent都进行了过滤。以parseRowsEvent方法为例:

com.alibaba.otter.canal.parse.inbound.mysql.dbsync.LogEventConvert#parseRowsEvent(省略部分代码片段)

  1. private Entry parseRowsEvent(RowsLogEvent event) {
  2. ...
  3. TableMapLogEvent table = event.getTable();
  4. String fullname = table.getDbName() + "." + table.getTableName();
  5. // check name filter
  6. if (nameFilter != null && !nameFilter.filter(fullname)) {
  7. return null;
  8. }
  9. if (nameBlackFilter != null && nameBlackFilter.filter(fullname)) {
  10. return null;
  11. }
  12. ...
  13. }

这里的nameFilter、nameBlackFilter实际上就是我们设置到parser中的 eventFilter、eventBlackFilter,只不过parser将其设置到LogEventConvert对象中换了一个名字。

可以看到,的确是先使用nameFilter进行白名单过滤,再使用nameBlackFilter进行黑名单过滤。在过滤时,使用dbName+"."+tableName作为参数,进行过滤。如果被过滤掉了,就返回null。

再次提醒,由于黑名单后过滤,因此如果希望订阅一个表,一定不要在黑名单中出现。

1.2 客户端配置

上面提到的都是服务端配置。canal也支持客户端配置过滤规则。举例来说,假设一个库有10张表,一个client希望订阅其中5张表,另一个client希望订阅另5张表。此时,服务端可以订阅10张表,当client来消费的时候,根据client的过滤规则只返回给对应的binlog event。

客户端指定过滤规则通过client模块中的CanalConnectorsubscribe方法来进行,subscribe有两种重载形式,如下:

  1. //对于第一个subscribe方法,不指定filter,以服务端的filter为准
  2. void subscribe() throws CanalClientException;
  3. // 指定了filter:
  4. // 如果本次订阅中filter信息为空,则直接使用canal server服务端配置的filter信息
  5. // 如果本次订阅中filter信息不为空,目前会直接替换canal server服务端配置的filter信息,以本次提交的为准
  6. void subscribe(String filter) throws CanalClientException;

通过不同client指定不同的过滤规则,可以达到服务端一份数据供多个client进行订阅消费的效果。

然而,想法是好的,现实确是残酷的,由于目前一个canal instance只允许一个client订阅,因此目前还达不到这种效果。读者明白这种设计的初衷即可。

最后列出filter模块的目录结构,这个模块的类相当的少,如下:

到此,filter模块的主要作用已经讲解完成。接着应该针对AviaterRegexFilter进行源码分析,由于其基于Aviator和oro基础之上编写,因此先对Aviator和oro进行介绍。

2 Aviator快速入门

说明,这里关于Aviator的相关内容直接摘录自官网:https://github.com/killme2008/aviator,并没有包含Aviator所有内容,仅仅是就canal内部使用到的一些特性进行讲解。

Aviator是一个高性能、轻量级的 java 语言实现的表达式求值引擎, 主要用于各种表达式的动态求值。现在已经有很多开源可用的 java 表达式求值引擎,为什么还需要 Avaitor 呢?

Aviator的设计目标是轻量级和高性能,相比于Groovy、JRuby的笨重, Aviator非常小, 加上依赖包也才 537K,不算依赖包的话只有 70K; 当然, Aviator的语法是受限的, 它不是一门完整的语言, 而只是语言的一小部分集合。

其次, Aviator的实现思路与其他轻量级的求值器很不相同, 其他求值器一般都是通过解释的方式运行, 而Aviator则是直接将表达式编译成 JVM 字节码, 交给 JVM 去执行。简单来说, Aviator的定位是介于 Groovy 这样的重量级脚本语言和 IKExpression 这样的轻量级表达式引擎之间。

Aviator 的特性:

  • 支持绝大多数运算操作符,包括算术操作符、关系运算符、逻辑操作符、位运算符、正则匹配操作符(=~)、三元表达式(?:)

  • 支持操作符优先级和括号强制设定优先级

  • 逻辑运算符支持短路运算。

  • 支持丰富类型,例如nil、整数和浮点数、字符串、正则表达式、日期、变量等,支持自动类型转换。

  • 内置一套强大的常用函数库

  • 可自定义函数,易于扩展

  • 可重载操作符

  • 支持大数运算(BigInteger)和高精度运算(BigDecimal)

  • 性能优秀

引入Aviator, 从 3.2.0 版本开始, Aviator 仅支持 JDK 7 及其以上版本。 JDK 6 请使用 3.1.1 这个稳定版本。

  1. <dependency>
  2. <groupId>com.googlecode.aviator</groupId>
  3. <artifactId>aviator</artifactId>
  4. <version>{version}</version>
  5. </dependency>

注意:canal 1.0.24 中使用的是Aviator 2.2.1版本。

Aviator的使用都是集中通过com.googlecode.aviator.AviatorEvaluator这个入口类来处理。在canal提供的AviaterRegexFilter中,仅仅使用到了Aviator部分功能,我们这里也仅仅就这些功能进行讲解。

2.1 编译表达式

参考:https://github.com/killme2008/aviator/wiki#%E7%BC%96%E8%AF%91%E8%A1%A8%E8%BE%BE%E5%BC%8F

案例:

  1. public class TestAviator {
  2. public static void main(String[] args) {
  3. //1、定义一个字符串表达式
  4. String expression = "a-(b-c)>100";
  5. //2、对表达式进行编译,得到Expression对象实例
  6. Expression compiledExp = AviatorEvaluator.compile(expression);
  7. //3、准备计算表达式需要的参数
  8. Map<String, Object> env = new HashMap<String, Object>();
  9. env.put("a", 100.3);
  10. env.put("b", 45);
  11. env.put("c", -199.100);
  12. //4、执行表达式,通过调用Expression的execute方法
  13. Boolean result = (Boolean) compiledExp.execute(env);
  14. System.out.println(result);  // false
  15. }
  16. }

通过compile方法可以将表达式编译成Expression的中间对象, 当要执行表达式的时候传入env并调用Expression的execute方法即可。 表达式中使用了括号来强制优先级, 这个例子还使用了>用于比较数值大小, 比较运算符!=、==、>、>=、<、<=不仅可以用于数值, 也可以用于String、Pattern、Boolean等等, 甚至是任何用户传入的两个都实现了java.lang.Comparable接口的对象之间。

编译后的结果你可以自己缓存, 也可以交给 Aviator 帮你缓存, AviatorEvaluator内部有一个全局的缓存池, 如果你决定缓存编译结果, 可以通过:

  1. public static Expression compile(String expression, boolean cached)

将cached设置为true即可, 那么下次编译同一个表达式的时候将直接返回上一次编译的结果。

使缓存失效通过以下方法:

  1. public static void invalidateCache(String expression)

2.2 自定义函数

参考:https://github.com/killme2008/aviator/wiki#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%87%BD%E6%95%B0

Aviator 除了内置的函数之外,还允许用户自定义函数,只要实现com.googlecode.aviator.runtime.type.AviatorFunction接口, 并注册到AviatorEvaluator即可使用. AviatorFunction接口十分庞大, 通常来说你并不需要实现所有的方法, 只要根据你的方法的参 数个数, 继承AbstractFunction类并override相应方法即可。

可以看一个例子,我们实现一个add函数来做数值的相加:

  1. //1、自定义函数AddFunction,继承AbstractFunction,覆盖其getName方法和call方法
  2. class AddFunction extends AbstractFunction {
  3. // 1.1 getName用于返回函数的名字,之后需要使用这个函数时,达表示需要以add开头
  4. public String getName() {
  5. return "add";
  6. }
  7. // 1.2 在执行计算时,call方法将会被回调。call方法有多种重载形式,参数可以分为2类:
  8. // 第一类:所有的call方法的第一个参数都是Map类型的env参数。
  9. // 第二类:不同数量的AviatorObject参数。由于在这里我们的add方法只接受2个参数,
  10. // 所以覆盖接受2个AviatorObject参数call方法重载形式
  11. // 用户在执行时,通过"函数名(参数1,参数2,...)"方式执行函数,如:"add(1, 2)"
  12. @Override
  13. public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
  14. Number left = FunctionUtils.getNumberValue(arg1, env);
  15. Number right = FunctionUtils.getNumberValue(arg2, env);
  16. return new AviatorDouble(left.doubleValue() + right.doubleValue());
  17. }
  18. }
  19. public class TestAviator {
  20. public static void main(String[] args) {
  21. //注册函数
  22. AviatorEvaluator.addFunction(new AddFunction());
  23. System.out.println(AviatorEvaluator.execute("add(1, 2)"));           // 3.0
  24. System.out.println(AviatorEvaluator.execute("add(add(1, 2), 100)")); // 103.0
  25. }
  26. }

注册函数通过AviatorEvaluator.addFunction方法, 移除可以通过removeFunction。另外, FunctionUtils 提供了一些方便参数类型转换的方法。

3 AviaterRegexFilter源码解析

AviaterRegexFilter实现了CanalEventParser接口,主要是实现其filter方法对binlog进行过滤。

首先对AviaterRegexFilter中定义的字段和构造方法进行介绍:

com.alibaba.otter.canal.filter.aviater.AviaterRegexFilter

  1. public class AviaterRegexFilter implements CanalEventFilter<String> {
  2. //我们的配置的binlog过滤规则可以由多个正则表达式组成,使用逗号”,"进行分割
  3. private static final String SPLIT = ",";
  4. //将经过逗号",”分割后的过滤规则重新使用|串联起来
  5. private static final String PATTERN_SPLIT = "|";
  6. //canal定义的Aviator过滤表达式,使用了regex自定义函数,接受pattern和target两个参数
  7. private static final String FILTER_EXPRESSION = "regex(pattern,target)";
  8. //regex自定义函数实现,RegexFunction的getName方法返回regex,call方法接受两个参数
  9. private static final RegexFunction regexFunction = new RegexFunction();
  10. //对自定义表达式进行编译,得到Expression对象
  11. private final Expression exp = AviatorEvaluator.compile(FILTER_EXPRESSION, true);
  12. static {
  13. //将自定义函数添加到AviatorEvaluator中
  14. AviatorEvaluator.addFunction(regexFunction);
  15. }
  16. //用于比较两个字符串的大小
  17. private static final Comparator<String> COMPARATOR = new StringComparator();
  18. //用户设置的过滤规则,需要使用SPLIT进行分割
  19. final private String pattern;
  20. //在没有指定过滤规则pattern情况下的默认值,例如默认为true,表示用户不指定过滤规则情况下,总是返回所有的binlog event
  21. final private boolean defaultEmptyValue;
  22. public AviaterRegexFilter(String pattern) {
  23. this(pattern, true);
  24. }
  25. //构造方法
  26. public AviaterRegexFilter(String pattern, boolean defaultEmptyValue) {
  27. //1 给defaultEmptyValue字段赋值
  28. this.defaultEmptyValue = defaultEmptyValue;
  29. //2、给pattern字段赋值
  30. //2.1 将传入pattern以逗号",”进行分割,放到list中;如果没有指定pattern,则list为空,意味着不需要过滤
  31. List<String> list = null;
  32. if (StringUtils.isEmpty(pattern)) {
  33. list = new ArrayList<String>();
  34. } else {
  35. String[] ss = StringUtils.split(pattern, SPLIT);
  36. list = Arrays.asList(ss);
  37. }
  38. //2.2 对list中的pattern元素,按照从长到短的排序
  39. Collections.sort(list, COMPARATOR);
  40. //2.3 对pattern进行头尾完全匹配
  41. list = completionPattern(list);
  42. //2.4 将过滤规则重新使用|串联起来赋值给pattern
  43. this.pattern = StringUtils.join(list, PATTERN_SPLIT);
  44. }
  45. ...
  46. }

上述代码中,2.2 步骤使用了COMPARATOR对list中分割后的pattern进行比较,COMPARATOR的类型是StringComparator,这是定义在AviaterRegexFilter中的一个静态内部类

  1. /**
  2. * 修复正则表达式匹配的问题,因为使用了 oro 的 matches,会出现:
  3. * foo|foot 匹配 foot 出错,原因是 foot 匹配了 foo 之后,会返回 foo,但是 foo 的长度和 foot 的长度不一样
  4. * 因此此类对正则表达式进行了从长到短的排序
  5. */
  6. private static class StringComparator implements Comparator<String> {
  7. @Override
  8. public int compare(String str1, String str2) {
  9. if (str1.length() > str2.length()) {
  10. return -1;
  11. } else if (str1.length() < str2.length()) {
  12. return 1;
  13. } else {
  14. return 0;
  15. }
  16. }
  17. }

上述代码2.3节调用completionPattern(list)方法对list中分割后的pattern进行头尾完全匹配

  1. /**
  2. * 修复正则表达式匹配的问题,即使按照长度递减排序,还是会出现以下问题:
  3. *  foooo|f.*t 匹配 fooooot 出错,原因是 fooooot 匹配了 foooo 之后,会将 fooo 和数据进行匹配,
  4. * 但是 foooo 的长度和 fooooot 的长度不一样,因此此类对正则表达式进行头尾完全匹配
  5. */
  6. private List<String> completionPattern(List<String> patterns) {
  7. List<String> result = new ArrayList<String>();
  8. for (String pattern : patterns) {
  9. StringBuffer stringBuffer = new StringBuffer();
  10. stringBuffer.append("^");
  11. stringBuffer.append(pattern);
  12. stringBuffer.append("$");
  13. result.add(stringBuffer.toString());
  14. }
  15. return result;
  16. }
  17. }

filter方法

AviaterRegexFilter类中最重要的就是filter方法,由这个方法执行过滤,如下:

  1. //1 参数:前面已经分析过parser模块的LogEventConvert中,会将binlog event的 dbName+”."+tableName当做参数过滤
  2. public boolean filter(String filtered) throws CanalFilterException {
  3. //2 如果没有指定匹配规则,返回默认值
  4. if (StringUtils.isEmpty(pattern)) {
  5. return defaultEmptyValue;
  6. }
  7. //3 如果需要过滤的dbName+”.”+tableName是一个空串,返回默认值
  8. //提示:一些类型的binlog event,如heartbeat,并不是真正修改数据,这种类型的event是没有库名和表名的
  9. if (StringUtils.isEmpty(filtered)) {
  10. return defaultEmptyValue;
  11. }
  12. //4 将传入的dbName+”."+tableName通过canal自定义的Aviator扩展函数RegexFunction进行计算
  13. Map<String, Object> env = new HashMap<String, Object>();
  14. env.put("pattern", pattern);
  15. env.put("target", filtered.toLowerCase());
  16. return (Boolean) exp.execute(env);
  17. }

第4步通过exp.execute方法进行过滤判断,前面已经看到,exp这个Expression实例是通过"regex(pattern,target)"编译得到。根据前面对AviatorEvaluator的介绍,其应该调用一个名字为regex的Aviator自定义函数,这个函数接受2个参数。

RegexFunction的实现如下所示:

com.alibaba.otter.canal.filter.aviater.RegexFunction

  1. public class RegexFunction extends AbstractFunction {
  2. public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
  3. String pattern = FunctionUtils.getStringValue(arg1, env);
  4. String text = FunctionUtils.getStringValue(arg2, env);
  5. Perl5Matcher matcher = new Perl5Matcher();
  6. boolean isMatch = matcher.matches(text, PatternUtils.getPattern(pattern));
  7. return AviatorBoolean.valueOf(isMatch);
  8. }
  9. public String getName() {
  10. return "regex";
  11. }
  12. }

可以看到,在这个函数里面,实际上是根据配置的过滤规则pattern,以及需要过滤的内容text(即dbName+”.”+tableName),通过jarkata-oro中Perl5Matcher类进行正则表达式匹配。

canal源码分析简介-3的更多相关文章

  1. 【Canal源码分析】Canal Instance启动和停止

    一.序列图 1.1 启动 1.2 停止 二.源码分析 2.1 启动 这部分代码其实在ServerRunningMonitor的start()方法中.针对不同的destination,启动不同的Cana ...

  2. 【Canal源码分析】Canal Server的启动和停止过程

    本文主要解析下canal server的启动过程,希望能有所收获. 一.序列图 1.1 启动 1.2 停止 二.源码分析 整个server启动的过程比较复杂,看图难以理解,需要辅以文字说明. 首先程序 ...

  3. 「从零单排canal 03」 canal源码分析大纲

    在前面两篇中,我们从基本概念理解了canal是一个什么项目,能应用于什么场景,然后通过一个demo体验,有了基本的体感和认识. 从这一篇开始,我们将从源码入手,深入学习canal的实现方式.了解can ...

  4. 【Canal源码分析】parser工作过程

    本文主要分析的部分是instance启动时,parser的一个启动和工作过程.主要关注的是AbstractEventParser的start()方法中的parseThread. 一.序列图 二.源码分 ...

  5. 【Canal源码分析】Sink及Store工作过程

    一.序列图 二.源码分析 2.1 Sink Sink阶段所做的事情,就是根据一定的规则,对binlog数据进行一定的过滤.我们之前跟踪过parser过程的代码,发现在parser完成后,会把数据放到一 ...

  6. 使用canal分析binlog(二) canal源码分析

    在能够跑通example后有几个疑问 1. canal的server端对于已经读取的binlog,client已经ack的position,是否持久化,保存在哪里 2. 即使不启动zookeeper, ...

  7. 【Canal源码分析】重要类图

    从Canal的整体架构中,我们可以看出,在Canal中,比较重要的一些领域有Parser.Sink.Store.MetaManager.CanalServer.CanalInstance.CanalC ...

  8. 【Canal源码分析】TableMetaTSDB

    这是Canal在新版本引入的一个内容,主要是为了解决由于历史的DDL导致表结构与现有表结构不一致,导致的同步失败的问题.采用的是Druid和Fastsql,来记录表结构到DB中,如果需要进行回滚时,得 ...

  9. 【Canal源码分析】整体架构

    本文详解canal的整体架构. 一.整体架构 说明: server代表一个canal运行实例,对应于一个jvm instance对应于一个数据队列 (1个server对应1..n个instance) ...

  10. 【Canal源码分析】配置项

    本文讲解canal中的一些配置含义. 一.配置加载图 二.配置文件canal.properties 2.1 common参数定义 比如可以将instance.properties的公用参数,抽取放置到 ...

随机推荐

  1. for Qbert sometimes we stay in lives == 0 condtion for a few frames —— baselines中环境包装器EpisodicLifeEnv的分析

    相关: baselines中环境包装器EpisodicLifeEnv的分析 一直不是很理解在reinforcement leanrning算法在atari游戏的observation的交互过程中对li ...

  2. Netty 如何自动探测内存泄露的发生

    本文基于 Netty 4.1.112.Final 版本进行讨论 本文是 Netty 内存管理系列的最后一篇文章,在第一篇文章 <聊一聊 Netty 数据搬运工 ByteBuf 体系的设计与实现& ...

  3. Python启动一个本地服务器文件下载

    日常工作中需要给同事分享下载链接,快速启动一个WebServer即可满足日常需求~ #安装软件 yum install screen -y #启动服务 python -m SimpleHTTPServ ...

  4. PHP数据库连接教程 - QSZ

    1准备工作 首先,确保你的环境中已安装: PHP 7.0+ MySQL/MariaDB Web服务器(Apache/Nginx) 2数据库连接代码 // config.php setAttribute ...

  5. Linux系统进程

    系统进程 [1].进程基本概述 当我们运行一个程序,那么我们将运行的程序叫进程 ​ PS1:当程序运行为进程后,系统会为该进程分配内存,以及进程运行的身份和权限 ​ PS2:在进程运行的过程中,服务器 ...

  6. 你不知道的5个JVM命令行标志

    本文是Neward & Associates的总裁Ted Neward为developerworks独家撰稿"你不知道5个--"系列的一篇文章:JVM有数百个命令行选项,在 ...

  7. Java的内存管理1:“并不只有C++程序员关心内存回收”——Java的内存管理2:"不中用的finalize( )方法"

    通常Java的缓存管理会由垃圾回收器(Java Garbage Collection)定时处理,无须程序员操心.但Java Garbage Collection仅有权回收那些非"强引用&qu ...

  8. Impala源代码分析(3)-backend查询执行过程

    4 Replies 这篇文章主要介绍impala-backend是怎么执行一个SQL Query的. 在Impala中SQL Query的入口函数是: void ImpalaServer::query ...

  9. ArrayList源码分析(基于JDK1.6)

    不积跬步,无以至千里:不积小流,无以成江海.从基础做起,一点点积累,加油! <Java集合类>中讲述了ArrayList的基础使用,本文将深入剖析ArrayList的内部结构及实现原理,以 ...

  10. js逆向之jsRpc

    github: https://github.com/jxhczhl/JsRpc 简介: 通过远程调用(rpc)的方式免去抠代码补环境 原理: 在网站的控制台新建一个WebScoket客户端链接到服务 ...