官方文档链接:https://docs.spring.io/spring-batch/trunk/reference/html/index.html
如有不正确的地方,欢迎下方留言指正。本文为自主原创,如有需要转载请与博主联系并说明出处,谢谢。
相信读者在看到这篇文章之前已经对spring batch已经有了初步了解,这里就不在赘述那些基本介绍(主要是不想去复制粘贴官方文档原话,既然是干货,就是做出干货的样子)
对于批处理框架,简单的处理应该没有太大问题,这里主要讨论复杂的4种模式。在开始前,还是想贴一段官方文档上的说明。
Many batch processing problems can be solved with single threaded, single process jobs, so it is always a good idea to properly check if that meets your needs before thinking about more complex implementations. Measure the performance of a realistic job and see if the simplest implementation meets your needs first: you can read and write a file of several hundred megabytes in well under a minute, even with standard hardware.
英文还行的应该能看懂这段的说明,中心思想是:在做任何复杂处理前最好先考虑清楚是否确实需要使用这些复杂的设置,以当今的计算机性能来说,简单单线程处理未必会不理想。
下面我们具体讨论四种模式
对于remote chunk这里不过多解释 不是本文的重点。官方文档的图应该还是比较清楚的,这里就引用一下图片。remote chunk适合主从机制。简单来说就是Master里有reader,把读出来的item交给中间件然后slave会去监听这个中间件。这里要注意的是只有在Master还没有达到瓶颈期的时候才能发挥作用,因为这种模式消耗比较大。(一般来说是这样) 可以这么打个比方,大家都吃过回转寿司,这里可以把中间的厨师比作Master,回转机器就是那个中间件,围坐在回转机器的顾客就是slave。厨师做好的寿司放在回转机器上(放进中间件),顾客看到自己想要的寿司就从机器上取下食用(消耗)掉。一般正常情况厨师不会无事可做,即使围了一圈的顾客,厨师也不至于忙的不可开交(没有达到瓶颈),顾客也不会迟迟吃不到自己的寿司,这就自动达到了某种平衡。
Parallel Step
这一种模式比较好理解,这里也不打算过多说明。唯一要注意的是,异步执行的step只有全部执行成功了才会继续后面的step。这里就引用一下官方文档的一个例子。
在这个例子中,step1,2,3,4分成了两个部分,准确的说是分成step1,2,3和step4两个部分。前面一个部分中又分了两个部分,以下是这个例子的模型。
这里要注意的是,只有在step1,2,3执行完毕后才会去执行step4。(这个地方有点类似于数据结构中图的拓扑结构)
下面就是要讲的重点:Multi-threaded Step和Partitioning of Step
很多人在这里可能会有误解。笔者在跟别人交流时,会有如下理解:
先来看一下xml怎么写的
这里只放出了部分的xml片段,下面是模型:
这里要指出的是这个chunk里面并没有包含reader和writer的行为(很多人一开始会受xml的影响,以为这个chunk会是一种“包装类”或“包装实体”)。其实这里的chunk应该理解成一种“容器”,或者说是事务的“长度”。上面xml所包含的信息应该理解成:
SELECT .......
SELECT .......
SELECT .......
SELECT .......
SELECT .......
SELECT .......
SELECT .......
SELECT .......
UPDATE/DELETE .....
commit
这是一次事务,里面包含了10次select操作(相当于reader),一次DML操作(相当于writer),然后提交。说到这里,肯定有人会问,既然是这样,那multi-threaded到底体现在哪里呢?下面是笔者的理解:
很明显,这里的step操作依然是单线程,这个模式下的多线程是体现在chunk上,多线程里只包含chunk。这个chunk就是Reader操作返回数据的一种“容器”。通过上面xml的例子可以知道,每个chunk的容量是10(就是10次读操作的数据) 。每次reader操作里return了一个数据,这个数据就会被多线程下的chunk“竞争”,谁争到就是谁的,然后这条线程的chunk就让出来,下次就不在参与竞争,每个chunk都竞争到一次后为一个轮次。一直持续10个轮次(这个数值就是上面xml中commit-interval的数值)。当一切都准备好了就开始一个chunk一个chunk去write,每个chunk里的数据都会被封装进List当做参数传给writer,一样也是哪个chunk竞争到writer哪个chunk里的数据被操作。
这里要注意1个细节:
上面xml中, 笔者"taskpool"的实现为
这是最简单的一种实现,这种实现没有什么太大的问题。但是如果你的实现是这样的:
那就要小心了,很明显你希望开启10个线程,但是spring batch真正启动的只有9个,剩下的一个线程并没有用来装chunk(关于这一点,笔者还没有研究到底用在哪,知道的朋友欢迎在下方留言)
官方文档的说法有点晦涩难懂,不妨先这样开始。(稍后我们会回来说明)
刚刚我们介绍了multi-threaded step,知道了实际上是对chunk多线程,并不是对step里的操作多线程。如果我想对step里面的reader和writer操作实现多线程操作除了自主实现外有没有别的办法。答案显然是有的,这里就要讲到最后一个模式——partitioning of step。当然此模式主要并不是为了解决multi-threaded的不足。
先来看一下基本模型
这个模型可以简单地看出,在这个模式下,step的操作真正意义上实现了多线程。然后刚刚也说了这个模式初衷并不是为了弥补multi-threaded的不足而是另有他用。再来看一下下面这个模型。
这里的data是reader要读的数据集合。所以,这个模式的目的是为了把要处理的数据进行分块,每个线程去执行自己对应那一块,这样就能充分发挥多线程的优势而不用去在意各个线程间的同步。具体操作如下:
首先看一下xml怎么写的:
上一段xml是定义一个partition handler,里面主要是把data“分割”的逻辑,你想把数据怎么切分就在handler里定义,handler的实体类就是下面bean定义的,既然要划分,那就得让spring batch知道你是怎么划分的,这里笔者就简单划分一下。
public class CusPartitioner implements Partitioner {
@Override
public Map partition(int gridSize) {
Map result = new HashMap<>();
int num = 10;
int start = 1;
int end = num;
System.out.println("---grid size: " + gridSize);
for (int i = 1; i <= gridSize; i++) {
ExecutionContext value = new ExecutionContext();
System.out.println("Thread" + i + "[" + start + "-" + end + "]");
value.putInt("start", start);
value.putInt("end", end);
value.putString("name", "Thread" + i);
result.put("partition: " + i, value);
start = end + 1;
end += num;
}
return result;
}
}
这个handler要实现Partitioner接口并重写partition这个方法。简单来说,定义一个map,里面包含一些参数(也就是要分割的参数)
这个时候我把reader的实现稍微修改一下,也就是用到上面handler的存进map的参数,在调用reader操作的时候,这些参数就会被传入进去,这样就可以真正实现数据“切分"。
最后我们回过头来看一下官方文档的说明:
第一幅图就是对应我们之前的模型,这里的slave就可以当做多线程的reader和writer,Master只起到一个”调用“作用,真正干活的是slave(这个也比较符合英文解释)。第二幅图就是执行的顺序,首先执行handler,让spring batch知道你想怎么划分,然后初始化你的参数(partitioner里面的map)最后调用step里的操作,操作完了返回给Master。
最后提醒一下这个模式的一个细节,就是参数gridSize不设置的话,默认是6