开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

10个必须了解的Linux系统进程

发表于 2021-08-20

本文已参与掘金创作者训练营第三期「高产更文」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。

前言

当我们习惯性的执行ps命令后会看到很多“奇奇怪怪”的进程,而这些进程大部门都是系统的内核进程。很多同学对之了解的甚少,因此今天就为大家整理一篇入门级的系统进程介绍,希望能够帮助大家对操作系统进程的理解。

在日常运维工作中,经常会看到一些奇怪的系统进程占用资源比较高。而且总是会听到业务线同学询问“xxx这个是啥进程啊?咋开启了这么多?”

而这些系统级的内核进程都是会用中括号括起来的,它们会执行一些系统的辅助功能(如将缓存写入磁盘),无括号的进程都是用户们执行的进程(如php、nginx等)。

如下图所示:

下面就为大家普及10个比较常见的系统进程:

  • kswapd0
  • kjournald
  • pdflush
  • kthreadd
  • migration
  • watchdog
  • events
  • kblockd
  • aio
  • rpciod

1 kswapd0

系统每过一定时间就会唤醒kswapd,看看内存是否紧张,如果不紧张,则睡眠,在kswapd中,有2个阀值,pages_hige和pages_low,当空闲内存页的数量低于pages_low的时候,kswapd进程就会扫描内存并且每次释放出32个free pages,直到free page的数量到达pages_high.

2 kjournald

journal:记录所有文件系统上的元数据改变,最慢的一种模式。

ordered:默认使用的模式,只记录文件系统改变的元数据,并在改变之前记录日志。

writeback :最快的一种模式,同样只记录修改过的元数据,依赖标准文件系统写进程将数据写到硬盘

3 pdflush

pdflush用于将内存中的内容和文件系统进行同步。

比如说:当一个文件在内存中进行修改,pdflush负责将它写回硬盘。每当内存中的垃圾页(dirty page)超过10%的时候,pdflush就会将这些页面备份回硬盘。这个比率是可调节的,通过/etc/sysctl.conf中的 vm.dirty_background_ratio项默认值为10也可以。

4 kthreadd

这种内核线程只有一个,它的作用是管理调度其它的内核线程。

它在内核初始化的时候被创建,会循环运行一个叫做kthreadd的函数,该函数的作用是运行kthread_create_list全局链表中维护的kthread。可以调用kthread_create创建一个kthread,它会被加入到kthread_create_list链表中,同时kthread_create会weak up kthreadd_task。kthreadd在执行kthread会调用老的接口——kernel_thread运行一个名叫“kthread”的内核线程去运行创建的kthread,被执行过的kthread会从kthread_create_list链表中删除,并且kthreadd会不断调用scheduler 让出CPU。这个线程不能关闭。

5 migration

这种内核线程共有32个,从migration/0到migration/31,每个处理器核对应一个migration内核线程,主要作用是作为相应CPU核的迁移进程,用来执行进程迁移操作,内核中的函数是migration_thread()

属于2.6内核的负载平衡系统,该进程在系统启动时自动加载(每个 cpu 一个),并将自己设为 SCHED_FIFO 的实时进程,然后检查 runqueue::migration_queue 中是否有请求等待处理,如果没有,就在 TASK_INTERRUPTIBLE 中休眠,直至被唤醒后再次检查。migration_thread() 仅仅是一个 CPU 绑定以及 CPU 电源管理等功能的一个接口。这个线程是调度系统的重要组成部分。

6 watchdog

这种内核线程共有32个,从watchdog/0到watchdog/31, 每个处理器核对应一个watchdog 内核线程,watchdog用于监视系统的运行,在系统出现故障时自动重新启动系统,包括一个内核 watchdog module 和一个用户空间的 watchdog 程序。

在Linux 内核下, watchdog的基本工作原理是:当watchdog启动后(即/dev/watchdog设备被打开后),如果在某一设定的时间间隔(1分钟)内/dev/watchdog没有被执行写操作, 硬件watchdog电路或软件定时器就会重新启动系统,每次写操作会导致重新设定定时器。

7 events

这种内核线程共有32个,从events/0到events/31, 每个处理器核对应一个 events内核线程。用来处理内核事件很多软硬件事件(比如断电,文件变更)被转换为events,并分发给对相应事件感兴趣的线程进行响应。

8 kblockd

这种内核线程共有32个,从kblockd/0到kblockd/31, 每个处理器核对应一个 kblockd 内核线程。用于管理系统的块设备,它会周期地激活系统内的块设备驱动。如果拥有块设备,那么这些线程就不能被去掉。

9 aio

这种内核线程共有32个,从aio/0到aio/31, 每个处理器核对应一个 aio 内核线程, 代替用户进程管理I/O,用以支持用户态的AIO(异步I/O),不应该被关闭。

10 rpciod

这种内核线程共有32个,从rpciod/0到rpciod/31, 每个处理器核对应一个rpciod内核线程,主要作用是作为远过程调用服务的守护进程,用于从客户端启动I/O服务,通常启动NFS服务时要用到它。

总结

进程是操作系统上非常重要的概念,所有系统上面跑的数据都会以进程的类型存在。在 Linux 系统当中:触发任何一个事件时,系统都会将它定义成为一个进程,所以,进程是Linux程序的唯一的实现方式。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用Python实现自动发消息,自定义内容,太省事了!

发表于 2021-08-20

有时候让了解放双手,让电脑来帮我们自动发一些我们想要发的消息,挺省力的,比如说白天写好了演讲稿,晚上要在群里进行文字演讲,那么我们就可以用脚本来实现自动复制、粘贴和发送文字的功能,从而解放我们自己,不用亲自在电脑上反复干这个Ctrl C/Ctrl V这个累活儿。

还可以把定时多长时间后发送指定内容,这下子就不用坐在电脑前面到点了发弹幕了。

在这里插入图片描述

多长时间发1条消息,又或者1秒发多少条信息,都可自由设置,时间设得短的话,一秒发几十条都没问题,只是太快了会形成刷屏的效果……

今天就把这个技巧给大家分享一下,很简单,没有多少代码。


一、效果

我们先来看一下效果,我这里设置的是4s后开始发送,间隔0.5s发一次。

[video(video-LQm2vy4y-1629256154101)(type-bilibili)(url-player.bilibili.com/player.html…(image-https://ss.csdn.net/p?http://i1.hdslb.com/bfs/archive/8a8f70a56e37a7458bba8bb829964ab8f60d8fb9.jpg)(title-%E7%94%A8Python%E5%AE%9E%E7%8E%B0%E7%94%B5%E8%84%91%E8%87%AA%E5%8A%A8%E5%8F%91%E6%B6%88%E6%81%AF%EF%BC%8C%E5%86%85%E5%AE%B9%E8%87%AA%E5%AE%9A%E4%B9%89%EF%BC%8C%E5%BF%AB%E6%85%A2%E5%9D%87%E5%8F%AF))]


二、开发环境

  • 系统:Windows10 64位
  • Python版本:3.9
  • Pycharm版本:2021.1.3
  • 模块(库):os、time、pyautogui、pyperclip

三、关键步骤解析

实现的代码文件主要有两个,目的分别是:获取聊天窗口位置和实现自动发送消息功能,用到的库在上面已经提过了,在开始写代码之前,先把要用的库先pip下载装好,下面就不再说这个了。

1.获取聊天窗口位置(源码1)

在我们发消息之前,得需要知道聊天窗口的位置在哪,即鼠标停留在哪里才能定位到聊天窗口的输入界面,也就是鼠标的x和y坐标是多少。

这里我用的是os、time和pyautogui这三个库,获取鼠标的实时位置的:

1
2
3
4
5
6
7
8
9
10
python复制代码try:
while True:
print("Press Ctrl-C to end")
x, y = pag.position() # 返回鼠标的坐标
posStr = "Position:" + str(x).rjust(4) + ',' + str(y).rjust(4)
print(posStr) # 打印坐标
time.sleep(0.2)
os.system('cls') # 清楚屏幕
except KeyboardInterrupt:
print('end....')

只要程序运行起来之后,每当我们移动鼠标,鼠标的x和y值就会自动发声改变并打印出来,我们只需要把聊天窗口调出来,把鼠标定位到聊天的窗口的输入位置就能获取到此时的x和y值,有了这个x和y值之后,我们才能告诉下面的发消息程序要在哪里进行粘贴和推送。

在这里插入图片描述
当然了,获取鼠标位置的方式有很多种,你们也可以去尝试一下其他方式的获取。

2.实现自动发送消息功能

在获取了x和y的值之后,我们要做的当然是写程序实现“复制文本→粘贴文本→发送消息”,这里就需要用到 pyautogui 来控制键盘和鼠标,用 pyperclip 来控制电脑进行复制和粘贴,以及用 time 这个库进行一下时间的控制。

首先我们把需要发送的内容提前准备好,放在content里面,到时候直接拿来用就可以了,内容可以自定义修改,比如这样的:

1
2
3
4
5
6
7
python复制代码content = """   
呼叫龙叔!
第二遍!
第三遍!
第四遍!
第五遍!
"""

我们在运行代码之后需要切换到聊天界面,中间需要时间去手动做一下这个操作,所以在复制粘贴和发送代码之前,我们需要留出一些时间给自己,我这里先设定了4s的时间延迟,当然也可以设置几个小时之后开始发消息。

1
python复制代码time.sleep(4)

接下来就是怎么实现复制粘贴和发送了:

1
2
3
4
5
6
7
python复制代码for line in list(content.split("\n"))*10:
if line:
pyautogui.click(669,687) #鼠标点击并定位到聊天窗口
pyperclip.copy(line) #复制该行
pyautogui.hotkey("ctrl","v") #粘贴,mac电脑则把ctrl换成command
pyautogui.typewrite("\n") #发送
time.sleep(5) #每次发完间隔5s

到了这里,所有的东西就已经完成了,如果觉得5s发送1条消息太快,可以修改time.sleep(5)里面的5这个数值,比如说10s发一条消息;如果你设置成0.01秒,那么就会是一个快速发消息的刷屏效果了…..

for循环中的“*10”控制循环次数,也就是让它发10次文本的样子,也可以设置不让它循环,把 list(content.split(“\n”))*10 改成 content.split(“\n”) 即可。

大致的方法就是上面这些,需要源码可以私聊我,你们也可以去尝试别的方式实现,说白了就是自动发消息,实现方式有很多种,比如更高级一点的直接带着xookie调api发送等等,以及按键精灵也是能实现这个功能,更多精彩,等你自己去挖掘了。


总结

这个脚本的本质是实现电脑自动发消息,只是间隔时间的设置导致它也具备快速发消息的功能,不仅仅是QQ,微信也是一样能用。

基本的原理就是这样了,你们还可以思考一下,如何在这个基础上,让程序在几个小时之后启动,间隔几十分钟发一次,彻底解放自己。
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

图解:为什么非公平锁的性能更高?

发表于 2021-08-20

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

在 Java 中 synchronized 和 ReentrantLock 默认使用的都是非公平锁,而它们采用非公平锁的原因都是一致的,都是为了提升程序的性能。那为什么非公平锁就能提升性能呢?接下来我们一起来看。

非公平锁

非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,任何线程在某时刻都有可能直接获取并拥有锁。
image.png
这就好比磊哥去加油,到了加油站之后发现前面有人在加,于是我就在车里刷起了抖音,过了一会,前面的车加完油走了,但磊哥没注意到,还在车里愉快的刷着抖音。然而此时加油站又来了一辆车,发现有空闲的油枪,于是就抢先在磊哥之前把油加了。这里的油枪就是锁,没有按照到达的先后顺序得到油枪,这就是非公平锁。
image.png

公平锁

公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
image.png
这就好像上高速排队过收费站一样,所有的车要排队等待通行,最先来的车最先通过收费站。
image.png

性能对比

公平锁和非公平锁的性能测试结果如下,以下测试数据来自于《Java并发编程实战》:

image.png
从上述结果可以看出,使用非公平锁的吞吐率(单位时间内成功获取锁的平均速率)要比公平锁高很多。

性能分析

以上测试数据虽然说明了结果,但并不能说明为什么非公平锁的性能会更高?所以,接下来,我们通过分析公平锁和非公平的执行流程,来得到这个问题的答案。

公平锁执行流程

获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。

用户态 & 内核态

用户态(User Mode):当进程在执行用户自己的代码时,则称其处于用户运行态。
内核态(Kernel Mode):当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态,此时处理器处于特权级最高的内核代码中执行。
image.png

为什么分内核态和用户态?

假设没有内核态和用户态之分,程序就可以随意读写硬件资源了,比如随意读写和分配内存,这样如果程序员一不小心将不适当的内容写到了不该写的地方,很可能就会导致系统崩溃。

而有了用户态和内核态的区分之后,程序在执行某个操作时会进行一系列的验证和检验之后,确认没问题之后才可以正常的操作资源,这样就不会担心一不小心就把系统搞坏的情况了,也就是有了内核态和用户态的区分之后可以让程序更加安全的运行,但同时两种形态的切换会导致一定的性能开销。

非公平锁执行流程

当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。

比如前几天磊哥去一个小营业厅办理网络移机的业务,去了之后发现前面有人在办业务,于是磊哥就告诉前面(办理业务)的小姐姐,“我门口休息一下,您等会办理完业务,麻烦去门口叫一下我”,小姐姐人也比较好,一口就答应下来了。但在小姐姐办完业务之后叫我,和我回到柜台办理业务之间,是有一段空闲时间的,这和等待队列中的线程被唤醒和恢复执行之间是有一段空闲时间是一样的,而在这个空闲的时间中,营业厅又来了一个老李头来交话费,等老李交完话费,我恰好也刚回来可以直接办理业务了,这样就是一个“三赢”的局面。老李头不用排在我后面等着缴话费,我也不用等老李头交完话费再办理移机,而且在单位时间内提高了营业员办理业务的效率,她也能早早的回家,这就是所谓的“三赢”。在更短的时间内执行更多的任务,这就是非公平锁的优势。
image.png
image.png

总结

本文我们介绍了公平锁和非公平锁的定义以及执行流程,从二者执行流程的细节可以看出,非公平锁因为不用按(顺)序执行,所以后来的锁也可以直接尝试获得锁,没有了阻塞和恢复执行的步骤,所以它的性能会更高。

本系列原创文章推荐

  1. 线程的 4 种创建方法和使用详解!
  2. Java中用户线程和守护线程区别这么大?
  3. 深入理解线程池 ThreadPool
  4. 线程池的7种创建方式,强烈推荐你用它…
  5. 池化技术到达有多牛?看了线程和线程池的对比吓我一跳!
  6. 并发中的线程同步与锁
  7. synchronized 加锁 this 和 class 的区别!
  8. volatile 和 synchronized 的区别
  9. 轻量级锁一定比重量级锁快吗?
  10. 这样终止线程,竟然会导致服务宕机?
  11. SimpleDateFormat线程不安全的5种解决方案!
  12. ThreadLocal不好用?那是你没用对!
  13. ThreadLocal内存溢出代码演示和原因分析!
  14. Semaphore自白:限流器用我就对了!
  15. CountDownLatch:别浪,等人齐再团!
  16. CyclicBarrier:人齐了,司机就可以发车了!
  17. synchronized 优化手段之锁膨胀机制!
  18. synchronized 中的 4 个优化,你知道几个?
  19. ReentrantLock 中的 4 个坑!

关注公号「Java中文社群」查看更多有意思、涨知识的 Java 并发文章。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

经典c语言程序100例 11-20例 【程序11】 【程序

发表于 2021-08-20

我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

【程序11】

题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?

  1. 程序分析: 兔子的规律为数列1,1,2,3,5,8,13,21….
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp复制代码#include "stdio.h"
#include "conio.h"
main()
{
long f1,f2;
int i;
f1=f2=1;
for(i=1;i<=20;i++)
{
printf("%12ld %12ld",f1,f2);
if(i%2==0) printf("\n"); /*控制输出,每行四个*/
f1=f1+f2; /*前两个月加起来赋值给第三个月*/
f2=f1+f2; /*前两个月加起来赋值给第三个月*/
}
getch();
}

【程序12】

题目:判断101-200之间有多少个素数,并输出所有素数。

  1. 程序分析:判断素数的方法:用一个数分别去除2到sqrt(这个数),如果能被整除,则表明此数不是素数,反之是素数。
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cpp复制代码#include "stdio.h"
#include "conio.h"
#include "math.h"
main()
{
int m,i,k,h=0,leap=1;
printf("\n");
for(m=101;m<=200;m++)
{
k=sqrt(m+1);
for(i=2;i<=k;i++)
if(m%i==0)
{
leap=0;
break;
}
if(leap)
{
printf("%-4d",m);
h++;
if(h%10==0)
printf("\n");
}
leap=1;
}
printf("\nThe total is %d",h);
getch();
}

【程序13】

题目:打印出所有的“水仙花数”,所谓“水仙花数”是指一个三位数,其各位数字立方和等于该数本身。例如:153是一个“水仙花数”,因为153=1的三次方+5的三次方+3的三次方。

  1. 程序分析:利用for循环控制100-999个数,每个数分解出个位,十位,百位。
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp复制代码#include "stdio.h"
#include "conio.h"
main()
{
int i,j,k,n;
printf("'water flower'number is:");
for(n=100;n<1000;n++)
{
i=n/100;/*分解出百位*/
j=n/10%10;/*分解出十位*/
k=n%10;/*分解出个位*/
if(i*100+j*10+k==i*i*i+j*j*j+k*k*k)
printf("%-5d",n);
}
getch();
}

【程序14】

题目:将一个正整数分解质因数。例如:输入90,打印出90=233*5。

程序分析:对n进行分解质因数,应先找到一个最小的质数k,然后按下述步骤完成:

  1. 如果这个质数恰等于n,则说明分解质因数的过程已经结束,打印出即可。
  2. 如果n<>k,但n能被k整除,则应打印出k的值,并用n除以k的商,作为新的正整数你n,
     重复执行第一步。
  3. 如果n不能被k整除,则用k+1作为k的值,重复执行第一步。

程序源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cpp复制代码/* zheng int is divided yinshu*/
#include "stdio.h"
#include "conio.h"
main()
{
int n,i;
printf("\nplease input a number:\n");
scanf("%d",&n);
printf("%d=",n);
for(i=2;i<=n;i++)
while(n!=i)
{
if(n%i==0)
{
printf("%d*",i);
n=n/i;
}
else
break;
}
printf("%d",n);
getch();
}

【程序15】

题目:利用条件运算符的嵌套来完成此题:学习成绩>=90分的同学用A表示,60-89分之间的用B表示,60分以下的用C表示。

  1. 程序分析:(a>b)?a:b这是条件运算符的基本例子。
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码#include "stdio.h"
#include "conio.h"
main()
{
int score;
char grade;
printf("please input a score\n");
scanf("%d",&score);
grade=score>=90?'A':(score>=60?'B':'C');
printf("%d belongs to %c",score,grade);
getch();
}

【程序16】

题目:输入两个正整数m和n,求其最大公约数和最小公倍数。

  1. 程序分析:利用辗除法。
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码#include "stdio.h"
#include "conio.h"
main()
{
int a,b,num1,num2,temp;
printf("please input two numbers:\n");
scanf("%d,%d",&num1,&num2);
if(num1<num2)/*交换两个数,使大数放在num1上*/
{
temp=num1;
num1=num2;
num2=temp;
}
a=num1;b=num2;
while(b!=0)/*利用辗除法,直到b为0为止*/
{
temp=a%b;
a=b;
b=temp;
}
printf("gongyueshu:%d\n",a);
printf("gongbeishu:%d\n",num1*num2/a);
getch();
}

【程序17】

题目:输入一行字符,分别统计出其中英文字母、空格、数字和其它字符的个数。

  1. 程序分析:利用while语句,条件为输入的字符不为 \n.
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cpp复制代码#include "stdio.h"
#include "conio.h"
main()
{
char c;
int letters=0,space=0,digit=0,others=0;
printf("please input some characters\n");
while((c=getchar())!='\n')
{
if(c>='a'&&c<='z'||c>='A'&&c<='Z')
letters++;
else if(c==' ')
space++;
else if(c>='0'&&c<='9')
digit++;
else
others++;
}
printf("all in all:char=%d space=%d digit=%d others=%d\n",letters,
space,digit,others);
getch();
}

【程序18】

题目:求s=a+aa+aaa+aaaa+aa…a的值,其中a是一个数字。例如2+22+222+2222+22222(此时共有5个数相加),几个数相加有键盘控制。

  1. 程序分析:关键是计算出每一项的值。
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cpp复制代码#include "stdio.h"
#include "conio.h"
main()
{
int a,n,count=1;
long int sn=0,tn=0;
printf("please input a and n\n");
scanf("%d,%d",&a,&n);
printf("a=%d,n=%d\n",a,n);
while(count<=n)
{
tn=tn+a;
sn=sn+tn;
a=a*10;
++count;
}
printf("a+aa+...=%ld\n",sn);
getch();
}

【程序19】

题目:一个数如果恰好等于它的因子之和,这个数就称为“完数”。例如6=1+2+3.编程找出1000以内的所有完数。

  1. 程序分析:请参照程序<–上页程序14.
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cpp复制代码#include "stdio.h"
#include "conio.h"
main()
{
static int k[10];
int i,j,n,s;
for(j=2;j<1000;j++)
{
n=-1;
s=j;
for(i=1;i<j;i++)
{
if((j%i)==0)
{
n++;
s=s-i;
k[n]=i;
}
}
if(s==0)
{
printf("%d is a wanshu",j);
for(i=0;i<n;i++)
printf("%d,",k[i]);
printf("%d\n",k[n]);
}
}
getch();
}

【程序20】

题目:一球从100米高度自由落下,每次落地后反跳回原高度的一半;再落下,求它在第10次落地时,共经过多少米?第10次反弹多高?

  1. 程序分析:见下面注释
  2. 程序源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cpp复制代码#include "stdio.h"
#include "stdio.h"
main()
{
float sn=100.0,hn=sn/2;
int n;
for(n=2;n<=10;n++)
{
sn=sn+2*hn;/*第n次落地时共经过的米数*/
hn=hn/2; /*第n次反跳高度*/
}
printf("the total of road is %f\n",sn);
printf("the tenth is %f meter\n",hn);
getch();
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

评分接口性能优化 10 倍 背景 优化过程 结果 结论

发表于 2021-08-19

背景

Helios 系统要处理的数据量比较大,尤其是查询所有服务一天的评分数据时要返回每日 1440 分钟的所有应用的评分,总计有几十万个数据点,接口有时延迟会达到数秒。本文记录如何利用 Arthas ,将接口从几百几千 ms,优化到几十 ms。

链路:

从链路上看,线上获取一整天的数据时大概 300 多 ms,而查询数据库只有 11ms,说明大部分时间都是程序组装数据时消耗的,于是动起了优化代码的念头。

优化过程

温馨提示:代码可以不用看,没有上下文的情况下很难明白函数什么意思。主要看 Arthas Trace 的结果与优化思路。

初始未优化版本

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Java复制代码    private HeliosGetScoreResponse queryScores(HeliosGetScoreRequest request) {
HeliosGetScoreResponse response = new HeliosGetScoreResponse();

List<HeliosScore> heliosScores = heliosService.queryScoresTimeBetween(request.getStartTime(), request.getEndTime(), request.getFilterByAppId());
if (CollectionUtils.isEmpty(heliosScores)) {
return response;
}

Set<String> dateSet = new HashSet<>();

Map<String, List<HeliosScore>> groupByAppIdHeliosScores = heliosScores.stream().collect(Collectors.groupingBy(HeliosScore::getAppId));
for (List<HeliosScore> value : groupByAppIdHeliosScores.values()) {
value.sort(Comparator.comparing(HeliosScore::getTimeFrom));
HeliosGetScoreResponse.Score score = new HeliosGetScoreResponse.Score();
score.setNamespace(value.get(0).getNamespace());
score.setAppId(value.get(0).getAppId());
for (HeliosScore heliosScore : value) {
List<HeliosScore> splitHeliosScores = heliosScore.split();
for (HeliosScore splitHeliosScore : splitHeliosScores) {
if (splitHeliosScore.getTimeFrom().compareTo(request.getStartTime()) < 0) {
continue;
}
if (splitHeliosScore.getTimeFrom().compareTo(request.getEndTime()) > 0) {
break;
}
dateSet.add(DateUtils.yyyyMMddHHmm.formatDate(splitHeliosScore.getTimeFrom()));
if (splitHeliosScore.getScores() == null) {
splitHeliosScore.setScores("100");
log.error("查询时发现数据缺失: {}", heliosScore);
}
score.add(Math.max(0, Integer.parseInt(splitHeliosScore.getScores())), null);
}
}
response.getValues().add(score);
}

response.setDates(new ArrayList<>(dateSet).stream().sorted().collect(Collectors.toList()));
return response;
}

Arthas Trace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Java复制代码`---ts=2021-08-17 16:28:00;thread_name=http-nio-8080-exec-10;id=81;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@20864cd1
`---[4046.398447ms] xxxService.controller.HeliosController:queryScores()
+---[0.022259ms] xxxService.model.helios.HeliosGetScoreResponse:<init>() #147
+---[0.007132ms] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #149
+---[0.006985ms] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #149
+---[0.008704ms] xxxService.model.helios.HeliosGetScoreRequest:getFilterByAppId() #149
+---[19.284658ms] xxxService.service.HeliosService:queryScoresTimeBetween() #149
+---[0.017468ms] org.apache.commons.collections.CollectionUtils:isEmpty() #150
+---[0.008054ms] java.util.HashSet:<init>() #154
+---[0.027591ms] java.util.List:stream() #156
+---[0.044229ms] java.util.stream.Collectors:groupingBy() #156
+---[0.155582ms] java.util.stream.Stream:collect() #156
+---[0.018318ms] java.util.Map:values() #157
+---[0.019199ms] java.util.Collection:iterator() #157
+---[min=3.51E-4ms,max=0.014266ms,total=0.125003ms,count=123] java.util.Iterator:hasNext() #157
+---[min=5.11E-4ms,max=0.010188ms,total=0.145693ms,count=122] java.util.Iterator:next() #157
+---[min=4.89E-4ms,max=0.045356ms,total=0.321978ms,count=122] java.util.Comparator:comparing() #158
+---[min=0.003637ms,max=0.033049ms,total=0.928795ms,count=122] java.util.List:sort() #158
+---[min=5.94E-4ms,max=0.010442ms,total=0.1485ms,count=122] xxxService.model.helios.HeliosGetScoreResponse$Score:<init>() #159
+---[min=4.5E-4ms,max=0.010857ms,total=0.12773ms,count=122] java.util.List:get() #160
+---[min=5.01E-4ms,max=0.007849ms,total=0.123696ms,count=122] xxxService.helios.entity.HeliosScore:getNamespace() #160
+---[min=6.5E-4ms,max=0.007324ms,total=0.135906ms,count=122] xxxService.model.helios.HeliosGetScoreResponse$Score:setNamespace() #160
+---[min=3.72E-4ms,max=0.010288ms,total=0.086703ms,count=122] java.util.List:get() #161
+---[min=5.1E-4ms,max=0.00627ms,total=0.103871ms,count=122] xxxService.helios.entity.HeliosScore:getAppId() #161
+---[min=5.97E-4ms,max=0.006531ms,total=0.126184ms,count=122] xxxService.model.helios.HeliosGetScoreResponse$Score:setAppId() #161
+---[min=4.45E-4ms,max=0.020198ms,total=0.138299ms,count=122] java.util.List:iterator() #162
+---[min=3.42E-4ms,max=0.014615ms,total=0.256056ms,count=366] java.util.Iterator:hasNext() #162
+---[min=3.59E-4ms,max=0.014974ms,total=0.174396ms,count=244] java.util.Iterator:next() #162
+---[min=0.071035ms,max=0.148132ms,total=19.444179ms,count=244] xxxService.helios.entity.HeliosScore:split() #163
+---[min=4.06E-4ms,max=0.022364ms,total=0.210152ms,count=244] java.util.List:iterator() #164
+---[min=3.07E-4ms,max=0.199649ms,total=143.267893ms,count=351604] java.util.Iterator:hasNext() #164
+---[min=3.25E-4ms,max=24.863976ms,total=177.15363ms,count=351360] java.util.Iterator:next() #164
+---[min=3.93E-4ms,max=0.096771ms,total=176.843018ms,count=351360] xxxService.helios.entity.HeliosScore:getTimeFrom() #165
+---[min=4.07E-4ms,max=18.772715ms,total=205.632183ms,count=351360] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #165
+---[min=3.33E-4ms,max=0.045589ms,total=149.24486ms,count=351360] java.util.Date:compareTo() #165
+---[min=3.93E-4ms,max=0.032972ms,total=86.466793ms,count=175680] xxxService.helios.entity.HeliosScore:getTimeFrom() #168
+---[min=4.12E-4ms,max=0.061003ms,total=94.294061ms,count=175680] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #168
+---[min=3.37E-4ms,max=0.038792ms,total=74.505056ms,count=175680] java.util.Date:compareTo() #168
+---[min=3.97E-4ms,max=0.036548ms,total=87.693935ms,count=175680] xxxService.helios.entity.HeliosScore:getTimeFrom() #171
1 +---[min=0.001952ms,max=0.068413ms,total=391.739063ms,count=175680] xxxService.utils.DateUtils$yyyyMMddHHmm:formatDate() #171
+---[min=4.07E-4ms,max=0.037904ms,total=108.107714ms,count=175680] java.util.Set:add() #171
+---[min=3.95E-4ms,max=0.031555ms,total=88.173857ms,count=175680] xxxService.helios.entity.HeliosScore:getScores() #172
+---[min=3.88E-4ms,max=0.033584ms,total=84.689466ms,count=175680] xxxService.helios.entity.HeliosScore:getScores() #176
+---[min=3.11E-4ms,max=0.038121ms,total=69.708752ms,count=175680] java.lang.Math:max() #176
+---[min=4.66E-4ms,max=0.03391ms,total=104.476576ms,count=175680] xxxService.model.helios.HeliosGetScoreResponse$Score:add() #176
+---[min=6.17E-4ms,max=0.01503ms,total=0.159826ms,count=122] xxxService.model.helios.HeliosGetScoreResponse:getValues() #179
+---[min=6.44E-4ms,max=0.03742ms,total=0.21068ms,count=122] java.util.List:add() #179
+---[0.108961ms] java.util.ArrayList:<init>() #182
+---[0.017455ms] java.util.ArrayList:stream() #182
+---[0.011099ms] java.util.stream.Stream:sorted() #182
+---[0.013699ms] java.util.stream.Collectors:toList() #182
+---[0.38178ms] java.util.stream.Stream:collect() #182
`---[0.004627ms] xxxService.model.helios.HeliosGetScoreResponse:setDates() #182

分析

Arthas 显示总共花了 4 秒,但实际上在链路上看大概是 350~450ms 左右。其他多出来的时间是 Arthas 每一次执行统计的消耗,因为方法里的循环比较多。这也告诉我们,不要用 trace 去看循环很多的方法。会对性能有非常严重的影响。

可以看出整个函数有 3 个循环,第一层循环的数量为 appId 的数量约为 140,第二层是查出来的数据条数,一天的数据已经归并了所以这里应该是 1,第三层是时间区间的分钟数,一天的话就是 1440 个。

Trace 中可以看到消耗最多的是封装的一个 SimpleDateFormat.formatDate()。

第一次优化

优化方向

  1. 遍历每个时间点的思路改变,把合并过的大对象拆分成一个个小对象直接遍历,改成先合并起来,通过时间点逻辑上遍历。这样会减少创建几十万个对象。
  2. 将时间点集合 Set<String> dateSet 改为 Set<Date>,这样减少反复 formatDate() 的开销。
  3. 优化字符串转数字的过程,减少 Integer.parseInt方法调用,改为用 Map<String, Integer> 提前创建出 0~100 的字符串数字字典。(后来经过 JMH 测试,还是 Integer.parseInt 最快)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Java复制代码private HeliosGetScoreResponse queryScores(HeliosGetScoreRequest request) {
HeliosGetScoreResponse response = new HeliosGetScoreResponse();

List<HeliosScore> heliosScoresRecord = heliosService.queryScoresTimeBetween(request.getStartTime(), request.getEndTime(), request.getFilterByAppId());
if (CollectionUtils.isEmpty(heliosScoresRecord)) {
return response;
}

Set<Date> dateSet = new HashSet<>();

List<HeliosScore> heliosScores = HeliosDataMergeJob.mergeData(heliosScoresRecord);

Map<String, List<HeliosScore>> groupByAppIdHeliosScores = heliosScores.stream().collect(Collectors.groupingBy(HeliosScore::getAppId));

for (List<HeliosScore> scores : groupByAppIdHeliosScores.values()) {
HeliosScore heliosScore = scores.get(0);
HeliosGetScoreResponse.Score score = new HeliosGetScoreResponse.Score();
score.setNamespace(heliosScore.getNamespace());
score.setAppId(heliosScore.getAppId());
score.setScores(new ArrayList<>());
response.getValues().add(score);

List<Integer> scoreIntList = HeliosHelper.splitScores(heliosScore);

// 以 requestTime 为准
Calendar indexDate = DateUtils.roundDownMinute(request.getStartTime().getTime());
int index = 0;
// 如果 timeFrom < requestTime,则增加 timeFrom 到 requestTime
while (indexDate.getTime().compareTo(heliosScore.getTimeFrom()) > 0) {
heliosScore.getTimeFrom().setTime(heliosScore.getTimeFrom().getTime() + 60_000);
index++;
}

while (indexDate.getTime().compareTo(request.getEndTime()) <= 0 && indexDate.getTime().compareTo(heliosScore.getTimeTo()) <= 0 && index < scoreIntList.size()) {
Integer scoreInt = scoreIntList.get(index++);
score.getScores().add(scoreInt);
dateSet.add(indexDate.getTime());
indexDate.add(Calendar.MINUTE, 1);
}
}

response.setDates(new ArrayList<>(dateSet).stream().sorted().map(DateUtils.yyyyMMddHHmm::formatDate).collect(Collectors.toList()));
return response;
}

Arthas Trace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Java复制代码---ts=2021-08-17 14:44:11;thread_name=http-nio-8080-exec-10;id=ab;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@16ea0f22
`---[6997.005629ms] xxxService.controller.HeliosController:queryScores()
+---[0.020032ms] xxxService.model.helios.HeliosGetScoreResponse:<init>() #149
+---[0.007451ms] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #151
+---[min=0.001054ms,max=7.458198ms,total=213.19538ms,count=170754] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #57
+---[0.007267ms] xxxService.model.helios.HeliosGetScoreRequest:getFilterByAppId() #57
+---[15.255919ms] xxxService.service.HeliosService:queryScoresTimeBetween() #57
+---[0.020045ms] org.apache.commons.collections.CollectionUtils:isEmpty() #152
+---[0.015161ms] java.util.HashSet:<init>() #156
+---[20.06713ms] xxxService.helios.jobs.HeliosDataMergeJob:mergeData() #158
+---[0.043042ms] java.util.List:stream() #160
+---[0.028232ms] java.util.stream.Collectors:groupingBy() #57
+---[min=0.087087ms,max=1.931641ms,total=2.018728ms,count=2] java.util.stream.Stream:collect() #57
+---[0.0151ms] java.util.Map:values() #162
+---[0.019611ms] java.util.Collection:iterator() #57
+---[min=7.55E-4ms,max=0.015165ms,total=0.201221ms,count=121] java.util.Iterator:hasNext() #57
+---[min=0.001178ms,max=0.02477ms,total=0.220931ms,count=120] java.util.Iterator:next() #57
+---[min=8.14E-4ms,max=0.01101ms,total=0.155044ms,count=120] java.util.List:get() #163
+---[min=0.001049ms,max=0.009425ms,total=0.231297ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:<init>() #164
+---[min=0.001167ms,max=0.009721ms,total=0.194502ms,count=120] xxxService.helios.entity.HeliosScore:getNamespace() #165
+---[min=0.001222ms,max=0.020409ms,total=0.264791ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setNamespace() #57
+---[min=0.001097ms,max=0.006475ms,total=0.169987ms,count=120] xxxService.helios.entity.HeliosScore:getAppId() #166
+---[min=0.00121ms,max=0.007106ms,total=0.207877ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setAppId() #57
+---[min=8.63E-4ms,max=0.008981ms,total=0.176195ms,count=120] java.util.ArrayList:<init>() #167
+---[min=0.001225ms,max=0.021948ms,total=0.340375ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setScores() #57
+---[min=0.00112ms,max=0.008984ms,total=0.196212ms,count=120] xxxService.model.helios.HeliosGetScoreResponse:getValues() #168
+---[min=7.64E-4ms,max=0.027237ms,total=154.660479ms,count=170753] java.util.List:add() #57
+---[min=0.028779ms,max=0.237608ms,total=20.049731ms,count=120] xxxService.helios.HeliosHelper:splitScores() #170
+---[min=0.001178ms,max=0.008102ms,total=0.199087ms,count=120] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #173
+---[min=6.89E-4ms,max=0.048069ms,total=140.74298ms,count=170040] java.util.Date:getTime() #57
+---[min=0.004686ms,max=0.03805ms,total=0.775394ms,count=120] xxxService.utils.DateUtils:roundDownMinute() #57
+---[min=7.84E-4ms,max=7.562581ms,total=162.855553ms,count=170040] java.util.Calendar:getTime() #176
2 +---[min=9.94E-4ms,max=0.029962ms,total=385.371864ms,count=339960] xxxService.helios.entity.HeliosScore:getTimeFrom() #57
1 +---[min=7.76E-4ms,max=7.936578ms,total=483.361269ms,count=511428] java.util.Date:compareTo() #57
+---[min=9.95E-4ms,max=0.077109ms,total=192.749805ms,count=169920] xxxService.helios.entity.HeliosScore:getTimeFrom() #177
+---[min=6.94E-4ms,max=7.358942ms,total=151.184751ms,count=169920] java.util.Date:setTime() #57
+---[min=7.67E-4ms,max=0.029244ms,total=152.500401ms,count=170753] java.util.Calendar:getTime() #181
+---[min=7.65E-4ms,max=0.016336ms,total=151.879643ms,count=170635] java.util.Calendar:getTime() #182
+---[min=0.001011ms,max=0.028133ms,total=196.192946ms,count=170635] xxxService.helios.entity.HeliosScore:getTimeTo() #57
+---[min=6.93E-4ms,max=0.836104ms,total=141.443001ms,count=170635] java.util.List:size() #57
+---[min=7.63E-4ms,max=7.940119ms,total=162.285955ms,count=170633] java.util.List:get() #183
3 +---[min=0.001068ms,max=0.973964ms,total=209.721ms,count=170633] xxxService.model.helios.HeliosGetScoreResponse$Score:getScores() #184
+---[min=7.71E-4ms,max=0.028856ms,total=154.918574ms,count=170633] java.util.Calendar:getTime() #185
+---[min=8.07E-4ms,max=8.030316ms,total=186.971072ms,count=170633] java.util.Set:add() #57
+---[min=7.82E-4ms,max=0.034732ms,total=156.2645ms,count=170633] java.util.Calendar:add() #186
+---[0.050615ms] java.util.ArrayList:<init>() #190
+---[0.019114ms] java.util.ArrayList:stream() #57
+---[0.029096ms] java.util.stream.Stream:sorted() #57
+---[0.018823ms] java.util.stream.Stream:map() #57
+---[0.009092ms] java.util.stream.Collectors:toList() #57
`---[0.006768ms] xxxService.model.helios.HeliosGetScoreResponse:setDates() #57

分析

这一步实际上执行时间优化了 50ms 左右。

从 Trace 中看耗时时间最长的是 Date 的 compareTo,也就是代码中的 if (splitHeliosScore.getTimeFrom().compareTo(request.getStartTime()) < 0)

而比较意外的是从对象中 get 属性居然也是有开销的。

第二次优化

优化方向

结合上一次 Arthas Trace 的结果,在以下几个方向进行优化:

  1. 将 Date 对象的换成 long 型时间戳进行比较
  2. 将 Date 对象反复 getTime、setTime,改为 long 型时间戳 += 60_000 实现,得到结果后只 setTime 一次。
  3. 每次填充数据都往 Set<String> dateSet 放入数据,改为通过标识判断只放入一次。
  4. 存放分数的 ArrayList 在第一次循环之后,可以确认大小,之后循环创建 ArrayList 时直接填入固定的大小,减少内存创建。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Java复制代码    private HeliosGetScoreResponse queryScores(HeliosGetScoreRequest request) {
HeliosGetScoreResponse response = new HeliosGetScoreResponse();

List<HeliosScore> heliosScoresRecord = heliosService.queryScoresTimeBetween(request.getStartTime(), request.getEndTime(), request.getFilterByAppId());
if (CollectionUtils.isEmpty(heliosScoresRecord)) {
return response;
}

Set<Date> dateSet = new HashSet<>();
boolean isDateSetInitial = false;
int scoreSize = 16;

List<HeliosScore> heliosScores = HeliosDataMergeJob.mergeData(heliosScoresRecord);

Map<String, List<HeliosScore>> groupByAppIdHeliosScores = heliosScores.stream().collect(Collectors.groupingBy(HeliosScore::getAppId));

for (List<HeliosScore> scores : groupByAppIdHeliosScores.values()) {
HeliosScore heliosScore = scores.get(0);
HeliosGetScoreResponse.Score score = new HeliosGetScoreResponse.Score();
score.setNamespace(heliosScore.getNamespace());
score.setAppId(heliosScore.getAppId());
score.setScores(new ArrayList<>(scoreSize));
response.getValues().add(score);

List<Integer> scoreIntList = HeliosHelper.splitScores(heliosScore);

// 以 requestTime 为准
long indexDateMills = request.getStartTime().getTime();
int index = 0;
// 如果 timeFrom < requestTime,则增加 timeFrom 到 requestTime
long heliosScoreTimeFromMills = heliosScore.getTimeFrom().getTime();
while (indexDateMills > heliosScoreTimeFromMills) {
heliosScoreTimeFromMills += 60_000;
index++;
}
heliosScore.getTimeFrom().setTime(heliosScoreTimeFromMills);

long requestEndTimeMills = request.getEndTime().getTime();
long heliosScoreTimeToMills = heliosScore.getTimeTo().getTime();
// 循环条件为 (当前时间 <= 请求最大时间) && (当前时间 <= 数据最大时间) && (index < 数据条数)
while (indexDateMills <= requestEndTimeMills && indexDateMills <= heliosScoreTimeToMills && index < scoreIntList.size()) {
score.getScores().add(scoreIntList.get(index++));
if (!isDateSetInitial) {
dateSet.add(new Date(indexDateMills));
}
indexDateMills += 60_000;
}
// 性能优化,减少重复放入的次数
isDateSetInitial = true;
// 性能优化,初始化足够的 size 减少扩容次数。 x1.1 为了万一数据数量不一致,留出一点 buffer。
scoreSize = (int) (score.getScores().size() * 1.1);
}

response.setDates(new ArrayList<>(dateSet).stream().sorted().map(DateUtils.yyyyMMddHHmm::formatDate).collect(Collectors.toList()));
return response;
}

Arthas Trace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Java复制代码`---ts=2021-08-17 15:20:41;thread_name=http-nio-8080-exec-7;id=aa;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@14be750c
`---[1411.395123ms] xxxService.controller.HeliosController:queryScores()
+---[0.016102ms] xxxService.model.helios.HeliosGetScoreResponse:<init>() #149
+---[0.019084ms] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #151
+---[0.007879ms] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #57
+---[0.006808ms] xxxService.model.helios.HeliosGetScoreRequest:getFilterByAppId() #57
+---[27.494178ms] xxxService.service.HeliosService:queryScoresTimeBetween() #57
+---[0.02087ms] org.apache.commons.collections.CollectionUtils:isEmpty() #152
+---[0.007694ms] java.util.HashSet:<init>() #156
+---[19.990512ms] xxxService.helios.jobs.HeliosDataMergeJob:mergeData() #160
+---[0.044161ms] java.util.List:stream() #162
+---[0.025737ms] java.util.stream.Collectors:groupingBy() #57
+---[min=0.079651ms,max=2.007048ms,total=2.086699ms,count=2] java.util.stream.Stream:collect() #57
+---[0.018405ms] java.util.Map:values() #164
+---[0.021408ms] java.util.Collection:iterator() #57
+---[min=7.4E-4ms,max=0.015625ms,total=0.177657ms,count=121] java.util.Iterator:hasNext() #57
+---[min=0.001193ms,max=0.026712ms,total=0.258491ms,count=120] java.util.Iterator:next() #57
+---[min=7.69E-4ms,max=0.011855ms,total=0.158671ms,count=120] java.util.List:get() #165
+---[min=0.001045ms,max=0.019788ms,total=0.232004ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:<init>() #166
+---[min=0.001072ms,max=0.007958ms,total=0.193652ms,count=120] xxxService.helios.entity.HeliosScore:getNamespace() #167
+---[min=0.001164ms,max=0.007796ms,total=0.201584ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setNamespace() #57
+---[min=0.001048ms,max=0.007456ms,total=0.178323ms,count=120] xxxService.helios.entity.HeliosScore:getAppId() #168
+---[min=0.001137ms,max=0.010225ms,total=0.201887ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setAppId() #57
+---[min=0.001627ms,max=0.010431ms,total=0.291395ms,count=120] java.util.ArrayList:<init>() #169
+---[min=0.00116ms,max=0.0088ms,total=0.20171ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setScores() #57
+---[min=0.001076ms,max=0.010293ms,total=0.199407ms,count=120] xxxService.model.helios.HeliosGetScoreResponse:getValues() #170
+---[min=7.54E-4ms,max=0.086952ms,total=150.86682ms,count=170753] java.util.List:add() #57
+---[min=0.020428ms,max=0.269554ms,total=19.477128ms,count=120] xxxService.helios.HeliosHelper:splitScores() #172
+---[min=0.001092ms,max=0.005258ms,total=0.202045ms,count=120] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #175
+---[min=7.09E-4ms,max=0.021027ms,total=0.630747ms,count=480] java.util.Date:getTime() #57
+---[min=0.00106ms,max=0.015055ms,total=0.188439ms,count=120] xxxService.helios.entity.HeliosScore:getTimeFrom() #178
+---[min=0.001025ms,max=0.009712ms,total=0.171506ms,count=120] xxxService.helios.entity.HeliosScore:getTimeFrom() #183
+---[min=7.4E-4ms,max=0.092253ms,total=0.251068ms,count=120] java.util.Date:setTime() #57
+---[min=0.001086ms,max=0.006234ms,total=0.184256ms,count=120] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #185
+---[min=0.001036ms,max=0.012332ms,total=0.176491ms,count=120] xxxService.helios.entity.HeliosScore:getTimeTo() #186
3 +---[min=6.73E-4ms,max=0.066785ms,total=135.009239ms,count=170635] java.util.List:size() #188
1 +---[min=0.001085ms,max=0.089243ms,total=208.003309ms,count=170633] xxxService.model.helios.HeliosGetScoreResponse$Score:getScores() #189
2 +---[min=7.31E-4ms,max=0.070823ms,total=145.488732ms,count=170633] java.util.List:get() #57
+---[min=0.001177ms,max=0.143546ms,total=2.319379ms,count=1440] java.util.Date:<init>() #191
+---[min=0.001346ms,max=0.064411ms,total=2.839878ms,count=1440] java.util.Set:add() #57
+---[min=0.001096ms,max=0.009059ms,total=0.190336ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:getScores() #198
+---[min=6.92E-4ms,max=0.016223ms,total=0.141751ms,count=120] java.util.List:size() #57
+---[0.069753ms] java.util.ArrayList:<init>() #201
+---[0.021066ms] java.util.ArrayList:stream() #57
+---[0.029498ms] java.util.stream.Stream:sorted() #57
+---[0.014089ms] java.util.stream.Stream:map() #57
+---[0.013053ms] java.util.stream.Collectors:toList() #57
`---[0.009818ms] xxxService.model.helios.HeliosGetScoreResponse:setDates() #57

分析

这一步将执行时间又优化了 80ms 左右。现在还剩是 160ms 了。

从 Trace 中看耗时时间最长的是三个方法:

  • getScores。直接 get 了属性啥也没干,但是积少成多
  • list.size()
  • list.get(index)

也就是说虽然这几个函数里也没干什么东西,但是函数调用、指针寻址本身也是有开销的。

第三次优化

优化方向

  1. 减少 list 属性的调用
  2. 一次次 list.add 方法改成 subList 一次性放入

也就是说循环中不做任何耗时操作,不做任何指针/引用。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
Java复制代码private HeliosGetScoreResponse queryScores(HeliosGetScoreRequest request) {
HeliosGetScoreResponse response = new HeliosGetScoreResponse();

List<HeliosScore> heliosScoresRecord = heliosService.queryScoresTimeBetween(request.getStartTime(), request.getEndTime(), request.getFilterByAppId());
if (CollectionUtils.isEmpty(heliosScoresRecord)) {
return response;
}

Set<Date> dateSet = new HashSet<>();
boolean isDateSetInitial = false;
int scoreSize = 16;

List<HeliosScore> heliosScores = HeliosDataMergeJob.mergeData(heliosScoresRecord);

Map<String, List<HeliosScore>> groupByAppIdHeliosScores = heliosScores.stream().collect(Collectors.groupingBy(HeliosScore::getAppId));

for (List<HeliosScore> scores : groupByAppIdHeliosScores.values()) {
HeliosScore heliosScore = scores.get(0);
HeliosGetScoreResponse.Score score = new HeliosGetScoreResponse.Score();
score.setNamespace(heliosScore.getNamespace());
score.setAppId(heliosScore.getAppId());
score.setScores(new ArrayList<>(scoreSize));
response.getValues().add(score);

List<Integer> scoreIntList = HeliosHelper.splitScores(heliosScore);

// 以 requestTime 为准
long indexDateMills = request.getStartTime().getTime();
int index = 0;
// 如果 timeFrom < requestTime,则增加 timeFrom 到 requestTime
long heliosScoreTimeFromMills = heliosScore.getTimeFrom().getTime();
while (indexDateMills > heliosScoreTimeFromMills) {
heliosScoreTimeFromMills += 60_000;
index++;
}
heliosScore.getTimeFrom().setTime(heliosScoreTimeFromMills);

long requestEndTimeMills = request.getEndTime().getTime();
long heliosScoreTimeToMills = heliosScore.getTimeTo().getTime();

// 循环条件为 (当前时间 <= 请求最大时间) && (当前时间 <= 数据最大时间) && (index < 数据条数)
int scoreIntListSize = scoreIntList.size();
int indexStart = index;
while (indexDateMills <= requestEndTimeMills && indexDateMills <= heliosScoreTimeToMills && index++ < scoreIntListSize) {
if (!isDateSetInitial) {
dateSet.add(new Date(indexDateMills));
}
indexDateMills += 60_000;
}
score.getScores().addAll(scoreIntList.subList(indexStart, index - 1));
// 性能优化,减少重复放入的次数
isDateSetInitial = true;
// 性能优化,初始化足够的 size 减少扩容次数。 x1.1 为了万一数据数量不一致,留出一点 buffer。
scoreSize = (int) (score.getScores().size() * 1.1);
}

response.setDates(new ArrayList<>(dateSet).stream().sorted().map(DateUtils.yyyyMMddHHmm::formatDate).collect(Collectors.toList()));
return response;
}

Arthas Trace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Java复制代码`---ts=2021-08-17 15:33:40;thread_name=http-nio-8080-exec-11;id=f1;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@d1c5cf2
`---[138.624811ms] xxxService.controller.HeliosController:queryScores()
+---[0.021852ms] xxxService.model.helios.HeliosGetScoreResponse:<init>() #149
+---[0.00746ms] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #151
+---[0.005838ms] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #57
+---[0.006341ms] xxxService.model.helios.HeliosGetScoreRequest:getFilterByAppId() #57
2 +---[15.227453ms] xxxService.service.HeliosService:queryScoresTimeBetween() #57
+---[0.02168ms] org.apache.commons.collections.CollectionUtils:isEmpty() #152
+---[0.008923ms] java.util.HashSet:<init>() #156
1 +---[22.703926ms] xxxService.helios.jobs.HeliosDataMergeJob:mergeData() #160
+---[0.047118ms] java.util.List:stream() #162
+---[0.043183ms] java.util.stream.Collectors:groupingBy() #57
+---[min=0.095654ms,max=2.183288ms,total=2.278942ms,count=2] java.util.stream.Stream:collect() #57
+---[0.022906ms] java.util.Map:values() #164
+---[0.025777ms] java.util.Collection:iterator() #57
+---[min=9.28E-4ms,max=0.017187ms,total=0.261862ms,count=121] java.util.Iterator:hasNext() #57
+---[min=9.88E-4ms,max=0.018901ms,total=0.280889ms,count=120] java.util.Iterator:next() #57
+---[min=9.65E-4ms,max=0.014741ms,total=0.262695ms,count=120] java.util.List:get() #165
+---[min=0.001215ms,max=0.013928ms,total=0.347762ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:<init>() #166
+---[min=0.001253ms,max=0.010855ms,total=0.328842ms,count=120] xxxService.helios.entity.HeliosScore:getNamespace() #167
+---[min=0.001316ms,max=0.014714ms,total=0.372553ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setNamespace() #57
+---[min=0.001211ms,max=0.010511ms,total=0.322723ms,count=120] xxxService.helios.entity.HeliosScore:getAppId() #168
+---[min=0.00132ms,max=0.010201ms,total=0.334627ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setAppId() #57
+---[min=0.00116ms,max=0.014504ms,total=0.386879ms,count=120] java.util.ArrayList:<init>() #169
+---[min=0.00131ms,max=0.014072ms,total=0.344922ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setScores() #57
+---[min=0.001261ms,max=0.017312ms,total=0.356444ms,count=120] xxxService.model.helios.HeliosGetScoreResponse:getValues() #170
+---[min=9.73E-4ms,max=0.016531ms,total=0.275794ms,count=120] java.util.List:add() #57
3 +---[min=0.023208ms,max=19.808819ms,total=47.196601ms,count=120] xxxService.helios.HeliosHelper:splitScores() #172
+---[min=0.001289ms,max=0.009578ms,total=0.36878ms,count=120] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #175
+---[min=8.85E-4ms,max=0.016405ms,total=0.994157ms,count=480] java.util.Date:getTime() #57
+---[min=0.001238ms,max=0.016801ms,total=0.34399ms,count=120] xxxService.helios.entity.HeliosScore:getTimeFrom() #178
+---[min=0.001217ms,max=0.008931ms,total=0.316197ms,count=120] xxxService.helios.entity.HeliosScore:getTimeFrom() #183
+---[min=9.14E-4ms,max=0.015929ms,total=0.277078ms,count=120] java.util.Date:setTime() #57
+---[min=0.001238ms,max=0.01061ms,total=0.3375ms,count=120] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #185
+---[min=0.001225ms,max=0.018059ms,total=0.315198ms,count=120] xxxService.helios.entity.HeliosScore:getTimeTo() #186
+---[min=8.79E-4ms,max=0.022669ms,total=0.272356ms,count=120] java.util.List:size() #189
+---[min=0.002001ms,max=0.056977ms,total=4.32853ms,count=1440] java.util.Date:<init>() #193
+---[min=0.002174ms,max=0.040594ms,total=4.594415ms,count=1440] java.util.Set:add() #57
+---[min=0.001302ms,max=0.012925ms,total=0.353165ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:getScores() #197
+---[min=0.001004ms,max=0.033424ms,total=0.338294ms,count=120] java.util.List:subList() #57
+---[min=0.004871ms,max=0.051046ms,total=2.945263ms,count=120] java.util.List:addAll() #57
+---[min=0.001291ms,max=0.009831ms,total=0.314292ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:getScores() #201
+---[min=8.84E-4ms,max=0.018168ms,total=0.249321ms,count=120] java.util.List:size() #57
+---[0.054305ms] java.util.ArrayList:<init>() #204
+---[0.024481ms] java.util.ArrayList:stream() #57
+---[0.028717ms] java.util.stream.Stream:sorted() #57
+---[0.013725ms] java.util.stream.Stream:map() #57
+---[0.0128ms] java.util.stream.Collectors:toList() #57
`---[0.007166ms] xxxService.model.helios.HeliosGetScoreResponse:setDates() #57

分析

这一步又优化了 100ms 左右,现在还剩 60ms。

现在从 trace 上看耗时操作只有三个了:

  • 查数据库
  • 合并数据
  • 拆分得分字符串 “100,100,100” 为 int 数组 [100,100,100]

第四次优化

优化方向

  1. 查数据库发现由于 SQL 判断不准确,每次会多查出来一条数据,在后边循环的时候会多循环一倍
  2. 合并数据时发现可以针对单条数据的情况直接过滤,减少开销。

代码

  1. 改了 SQL 并验证,减少查询出来的数据量
  2. 单条数据时不再处理合并逻辑

Arthas Trace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Java复制代码`---ts=2021-08-17 16:03:24;thread_name=http-nio-8080-exec-13;id=f1;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@69e2fe3b
`---[38.171379ms] xxxService.controller.HeliosController:queryScores()
+---[0.009463ms] xxxService.model.helios.HeliosGetScoreResponse:<init>() #149
+---[0.00348ms] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #151
+---[0.003233ms] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #57
+---[0.003395ms] xxxService.model.helios.HeliosGetScoreRequest:getFilterByAppId() #57
1 +---[10.157226ms] xxxService.service.HeliosService:queryScoresTimeBetween() #57
+---[0.009989ms] org.apache.commons.collections.CollectionUtils:isEmpty() #152
+---[0.003394ms] java.util.HashSet:<init>() #156
+---[0.083535ms] xxxService.helios.jobs.HeliosDataMergeJob:mergeData() #160
+---[0.017819ms] java.util.List:stream() #162
+---[0.011787ms] java.util.stream.Collectors:groupingBy() #57
+---[min=0.047561ms,max=2.02786ms,total=2.075421ms,count=2] java.util.stream.Stream:collect() #57
+---[0.015525ms] java.util.Map:values() #164
+---[0.021965ms] java.util.Collection:iterator() #57
+---[min=7.25E-4ms,max=0.009733ms,total=0.115783ms,count=121] java.util.Iterator:hasNext() #57
+---[min=8.43E-4ms,max=0.011422ms,total=0.142771ms,count=120] java.util.Iterator:next() #57
+---[min=7.81E-4ms,max=0.010883ms,total=0.128809ms,count=120] java.util.List:get() #165
+---[min=0.001023ms,max=0.004301ms,total=0.150165ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:<init>() #166
+---[min=0.001066ms,max=0.004648ms,total=0.154698ms,count=120] xxxService.helios.entity.HeliosScore:getNamespace() #167
+---[min=0.001137ms,max=0.005607ms,total=0.170279ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setNamespace() #57
+---[min=0.001023ms,max=0.004292ms,total=0.151767ms,count=120] xxxService.helios.entity.HeliosScore:getAppId() #168
+---[min=0.001105ms,max=0.004701ms,total=0.164955ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setAppId() #57
+---[min=0.001359ms,max=0.007931ms,total=0.233665ms,count=120] java.util.ArrayList:<init>() #169
+---[min=0.001117ms,max=0.00785ms,total=0.168539ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:setScores() #57
+---[min=0.001073ms,max=0.004488ms,total=0.156654ms,count=120] xxxService.model.helios.HeliosGetScoreResponse:getValues() #170
+---[min=7.98E-4ms,max=0.00977ms,total=0.125818ms,count=120] java.util.List:add() #57
+---[min=0.022304ms,max=0.12093ms,total=8.88628ms,count=120] xxxService.helios.HeliosHelper:splitScores() #172
+---[min=0.001092ms,max=0.004967ms,total=0.161288ms,count=120] xxxService.model.helios.HeliosGetScoreRequest:getStartTime() #175
+---[min=7.02E-4ms,max=0.012136ms,total=0.467786ms,count=480] java.util.Date:getTime() #57
+---[min=0.001022ms,max=0.004944ms,total=0.151353ms,count=120] xxxService.helios.entity.HeliosScore:getTimeFrom() #178
+---[min=0.001018ms,max=0.004731ms,total=0.148025ms,count=120] xxxService.helios.entity.HeliosScore:getTimeFrom() #183
+---[min=7.3E-4ms,max=0.009359ms,total=0.120588ms,count=120] java.util.Date:setTime() #57
+---[min=0.00107ms,max=0.008948ms,total=0.162848ms,count=120] xxxService.model.helios.HeliosGetScoreRequest:getEndTime() #185
+---[min=0.001034ms,max=0.014003ms,total=0.158614ms,count=120] xxxService.helios.entity.HeliosScore:getTimeTo() #186
+---[min=6.99E-4ms,max=0.009995ms,total=0.11179ms,count=120] java.util.List:size() #189
+---[min=6.95E-4ms,max=0.005468ms,total=1.116308ms,count=1440] java.util.Date:<init>() #193
+---[min=7.79E-4ms,max=0.029909ms,total=1.407528ms,count=1440] java.util.Set:add() #57
+---[min=0.001097ms,max=0.008616ms,total=0.160597ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:getScores() #197
+---[min=8.23E-4ms,max=0.0294ms,total=0.153353ms,count=120] java.util.List:subList() #57
+---[min=0.005771ms,max=0.04465ms,total=1.992151ms,count=120] java.util.List:addAll() #57
+---[min=0.001098ms,max=0.007013ms,total=0.169555ms,count=120] xxxService.model.helios.HeliosGetScoreResponse$Score:getScores() #201
+---[min=7.04E-4ms,max=0.01315ms,total=0.120998ms,count=120] java.util.List:size() #57
+---[0.197732ms] java.util.ArrayList:<init>() #204
+---[0.018589ms] java.util.ArrayList:stream() #57
+---[0.025192ms] java.util.stream.Stream:sorted() #57
+---[0.012544ms] java.util.stream.Stream:map() #57
+---[0.012188ms] java.util.stream.Collectors:toList() #57
`---[0.0067ms] xxxService.model.helios.HeliosGetScoreResponse:setDates() #57

分析

可以看到现在最大耗时的地方终于是数据库查询了。现在查询一整天的数据,也只需要 25~40ms 左右。

结果

链路:

链路上看程序代码还是要处理个十几 ms,主要是字符串转 int[] 时的开销,这一步可以再想办法继续优化。

结论

从这次优化我们可以得到一些结论:

  1. 尽量少创建对象
  2. SimpleDateFormat的开销很大
  3. Date.compare 的开销不低
  4. 哪怕最简单的操作如 list.size() list.add次数多了开销也很可观
  5. 对于性能分析和优化一定要有合适工具,才能得出有用的结论并针对性优化。一开始我以为减少对象创建就万事大吉,但实际上性能消耗的大头并不在这里。还是得借助 Arthas 的 Trace 才能真正针对性地优化。

强烈推荐 动态追踪技术漫谈,每个希望提升的工程师都应该深入了解动态追踪技术。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Mybatis】Mybatis源码之resultMap标签

发表于 2021-08-19

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

时序图

XMLMapperBuilderResultMapResolverMapperBuilderAssistantResultMap.BuilderresultMapElementsresultMapElementresolveaddResultMapbuildResultMapXMLMapperBuilderResultMapResolverMapperBuilderAssistantResultMap.Builder
详细步骤
====

XMLMapperBuilder#configurationElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码/**
* 解析映射文件的下层节点
* @param context 映射文件根节点
*/
private void configurationElement(XNode context) {
try {
// 读取当前映射文件namespace
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 映射文件中其他配置节点的解析
// 解析缓存标签
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
// 解析参数映射
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析结果集映射
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析sql标签
sqlElement(context.evalNodes("/mapper/sql"));
// 处理各个数据库操作语句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}

XMLMapperBuilder#resultMapElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
java复制代码private void resultMapElements(List<XNode> list) throws Exception {
// 遍历
for (XNode resultMapNode : list) {
try {
// 处理单个resultMap标签
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
return resultMapElement(resultMapNode, Collections.emptyList(), null);
}

/**
* 兼容处理
*/
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
/**
* 所有类型的resultMap相关类型标签都可处理
* 针对不同类型的resultMap标签,通过不同的属性来获取type
*/
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 通过别名来解析获取typeClass
Class<?> typeClass = resolveClass(type);
// 获取继承的typeClass
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>();
//将已解析的标签放入resultMappings
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
// 解析resultMap标签下的所有子标签
for (XNode resultChild : resultChildren) {
// 解析构造标签constructor
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
}
// 解析discriminator鉴别器标签
else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
}
// 解析普通标签
else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
// 构建ResultMapping对象(若未指定javaType,则根据属性的set方法参数类型来设置javaType,并获取对应的TypeHandler),并添加到容器中
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// 创建解析器
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
// 解析
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}

ResultMapResolver#resolve

1
2
3
4
5
6
7
java复制代码/**
* 完成 ResultMap 的继承关系解析,并将解析的结果放入Configuration中的resultMaps中
* @return
*/
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
}

MapperBuilderAssistant#addResultMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
java复制代码/**
* 创建结果映射对象
* @param id 输入参数参照 ResultMapResolver 属性
* @return ResultMap对象
*/
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
id = applyCurrentNamespace(id, false);
extend = applyCurrentNamespace(extend, true);

// 解析ResultMap的继承关系
if (extend != null) {
// 判断configuration对象中是否存在extend指向的ResultMap,如果没有,则抛出异常,在后续操作中再次对当前ResultMap进行解析处理
if (!configuration.hasResultMap(extend)) {
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
// 获取父级的ResultMap
ResultMap resultMap = configuration.getResultMap(extend);
// 获取父级的属性映射
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
// 删除父类映射中在子类映射中已经存在的属性,从而使用子类映射覆盖父类映射
extendedResultMappings.removeAll(resultMappings);
// Remove parent constructor if this resultMap declares a constructor.
// 如果当前子类ResultMap设置有构建器,则移除父级构建器
boolean declaresConstructor = false;
for (ResultMapping resultMapping : resultMappings) {
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
if (declaresConstructor) {
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
// 最终从父级继承而来的所有属性映射
resultMappings.addAll(extendedResultMappings);
}
// 创建当前的ResultMap
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
// 将当前的ResultMap加入configuration
configuration.addResultMap(resultMap);
return resultMap;
}

ResultMap#build

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
java复制代码public ResultMap build() {
if (resultMap.id == null) {
throw new IllegalArgumentException("ResultMaps must have an id");
}
resultMap.mappedColumns = new HashSet<>();
resultMap.mappedProperties = new HashSet<>();
resultMap.idResultMappings = new ArrayList<>();
resultMap.constructorResultMappings = new ArrayList<>();
resultMap.propertyResultMappings = new ArrayList<>();
final List<String> constructorArgNames = new ArrayList<>();
for (ResultMapping resultMapping : resultMap.resultMappings) {
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
final String column = resultMapping.getColumn();
if (column != null) {
// 在已映射的列中添加当前列,名称转换为全大写
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
} else if (resultMapping.isCompositeResult()) {
for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
final String compositeColumn = compositeResultMapping.getColumn();
if (compositeColumn != null) {
resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
}
}
}
final String property = resultMapping.getProperty();
if (property != null) {
// 在已映射的属性中添加当前属性
resultMap.mappedProperties.add(property);
}
// 如果是constructor标签下的映射关系,则放入constructorResultMappings中
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
resultMap.constructorResultMappings.add(resultMapping);
if (resultMapping.getProperty() != null) {
// 如果映射关系中name属性不为空,则放入constructorArgNames中
constructorArgNames.add(resultMapping.getProperty());
}
} else {
resultMap.propertyResultMappings.add(resultMapping);
}
// 将id放入idResultMappings中(id标签和idArg标签)
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
resultMap.idResultMappings.add(resultMapping);
}
}
// 如果idResultMappings为空,则将所有映射放入
if (resultMap.idResultMappings.isEmpty()) {
resultMap.idResultMappings.addAll(resultMap.resultMappings);
}
// 当constructorArgNames不为空时
if (!constructorArgNames.isEmpty()) {
// 根据配置的参数映射获取对应构造方法中的参数列表
final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
if (actualArgNames == null) {
throw new BuilderException("Error in result map '" + resultMap.id
+ "'. Failed to find a constructor in '"
+ resultMap.getType().getName() + "' by arg names " + constructorArgNames
+ ". There might be more info in debug log.");
}
// 将映射关系constructorResultMappings按照获取的参数顺序排序
resultMap.constructorResultMappings.sort((o1, o2) -> {
int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
return paramIdx1 - paramIdx2;
});
}
// lock down collections
// 锁定映射的结果集
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
return resultMap;
}

以上便是Mybatis解析resultMap标签的过程。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

站在思想层面看MVX架构

发表于 2021-08-19

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

程序的本质

程序的本质在于模拟现实,但是有更明确的分工

简单的一个例子: 我 写 代码。

这是一个主谓结构: 主语->我,谓语->写,宾语->代码。

现在让我们来面向视角看问题:

  • 代码: 是个物体,是用来 被 写 的
  • 写: 是个动作,是用来 被 我执行的
  • 我: 是个物体,是用来 执行 写 这个动作 写代码的。

好,接着我们来面向对象写代码:

首先,创建一个我,这是个物体,所以应该创建一个对象:

1
2
3
kotlin复制代码public class Me {

}

然后,需要有代码,才能写,代码也是一个物体,那么再创建一个对象:

1
2
3
kotlin复制代码public class Code {

}

等等,代码应该有内容,有注释,好,我们来简单模拟下(程序就是模拟现实的):

1
2
3
4
5
6
arduino复制代码public class Code {
// 代码
public String code;
// 注释
public String comment;
}

最后,需要创建一个写的动作,写既然是一个动作,不是物体,那么肯定是属于某个物体的行为,这里就是我的行为,动作就是函数(接口),于是就在 “我” 里面添加函数:

1
2
3
4
5
6
7
8
csharp复制代码public class Me {
// 添加写的行为,写什么?写代码
public Code write() {
Code code = new Code();
code.code = "This is code";
return code;
}
}

这里有个问题,写过的代码,怎么展示出来呢,我们需要个显示器来显示,显示器是物体,所以我们需要定义个对象:

1
2
3
4
5
6
typescript复制代码public class Display {
// 显示器可以显示内容
public void display(String content) {
System.out.println(content);
}
}

然后,我们需要展示我们的代码,我们可以直接这样改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码public class Me {
// 添加写的行为,写什么?写代码
public Code write() {

// 写代码
Code code = new Code();
code.code = "This is code";

// 展示
Display display = new Display();
display.display(code.code);
return code;
}
}

这样当然没问题,但是,write()明明是一个写的函数,却额外做了展示的事情,不满足SRP,万一我只想写,不想展示呢,所以我们将函数分离职责,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class Me {
// 添加写的行为,写什么?写代码
public Code write() {

// 写代码
Code code = new Code();
code.code = "This is code";
return code;
}

// 展示代码
public void showCode(){

// 写代码
Code code = write();

// 展示
Display display = new Display();
display.display(code.code);
}
}

这样也不对,因为showCode()里面又调用了写的动作,showCode()应该只负责展示代码的,怎么办呢?追起根源,展示代码这个动作,并不是我自己的行为,所以不应该放在Me里面,任何人都可以展示代码,比如,我把自己的代码提供给第三方,第三方只要拿着显示器,就能展示出来,所以,展示代码这个事情,应该是属于第三方的,好,现在我们把Me里面的showCode()删掉,创建一个第三方场景类:

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码public class Client {
// 展示代码
public void showCode(){

// 我来提供代码
Me me = new Me();
Code code = me.write();

// 让显示器来展示
Display display = new Display();
display.display(code.code);
}
}

上面我们绕了一大圈,最后也就这么一句话: 我在Client中写了代码,然后把它展示了出来。用程序的话来说就是 Client控制我写出Code,然后控制Display展示Code。

这里我们就引出了最基础的架构思想: MVC。MVC的核心就是一句话: C控制M展示在V上,这里Client就是C,Code就是M,Display就是V,所以是Client控制Code展示在Display上。至于Me,是负责提供生产Code的,就像是服务器是负责提供数据的一样。

MVC

MVC就是:Model,View,Controller的简写,核心是职责分离

我们知道,计算机由: 控制器,运算器,存储器,输入设备和输出设备组成,这里就是: 控制器 控制 存储器里面的内容 展示在 输出设备 上。所以MVC是个广义的思想,他不是架构,是思想,可以是物理的,也可以是虚拟的。

MVC的核心就是一句话: C控制M展示在V上,精粹就是四个字职责分离。

这里再强调一下,MVC是广义的概念,广义的就是思想,不是架构,或者从狭义来说,它也是一种架构。

我们来看下MVC的结构图:

MVC

这里我们可以看到,MVC本身的耦合是挺严重的,M和V竟然也有关联,这确实不应该的。但是MVC的核心是职责分离而不是解耦合,体现在设计上就是,MVC的核心是单一职责,而不是最少知识。

在普通的Android应用中,M就是数据,V就是xml布局,C明显就是Activity。这里有点不太对劲儿,Activity明明更像一个View,因为它有findViewById()的方法,为什么又是C呢,这岂不是违背了MVC的职责分离明确的原则吗?这只能说谷歌设计的不太好。于是就有了下面的MVP模式。

MVP

MVP就是:Model,View,Presenter,这里把Controller替换为了Presenter。

MVP的核心除了MVC的职责分离,还有解耦合,也就是说,他在满足单一职责的基础上,又满足了最少知识原则。我们看下它的结构图:

MVP

这里我们看到,View和Model没有关联了,它们都通过Presenter来沟通,是不是有点像中介者模式。中介者模式的优点不就是解耦合吗,正好!

中介者模式

现在假如我们使用了MVP模式,我们的代码看起来是这样:

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class Model {
Presenter presenter;
}

class View {
Presenter presenter;
}

class Presenter {
Model model;
View view;
}

可以看到,View和Model是零耦合的。

那么事件的流向就是: View -> Presenter -> Model

数据的流向就是: Model -> Presenter -> View

中间都需要经过Presenter。

当我们在Android中使用时,可以把Activity当作Presenter,把xml当作View,当然也可以直接把Activity抽空,自定义一个Presenter。这里我们来个简单的例子示例一下MVP中事件和数据的流向。

假设现在屏幕上有一个按钮,点击之后需要展示数据,事件肯定是屏幕引起的,也就是View,所以事件的出发点是View。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
typescript复制代码public class View {
Presenter presenter;
// 1 发出事件
public void click(){
presenter.getInfo();
}

// 6 接收并展示数据
public void showInfo(String info){
setInto(info);
}
}

public class Presenter {
Model model;
View view;

// 2 传递事件
public void getInfo(){
model.getInfo();
}

// 5 传递数据
public void onGetInfo(String info){
view.showInfo(info);
}
}

public class Model {
Presenter presenter;

// 3 接收并处理事件
public void getInfo(){
String info = getInfoFromServer();

// 4 发出数据
presenter.onGetInfo(info);
}

// 模拟从服务器获取数据
private String getInfoFromServer(){
String info = "this is info";
return info;
}
}

事件的流向(1->2->3): View.click() -> presenter.getInfo() -> model.getInfo() -> 从服务器获取数据。

数据的流向(4->5->6): model.getInfo() -> presenter.onGetInfo(info) -> view.showInfo(info) -> 展示在屏幕上。

可以看到,View是事件的发起者和数据的接收者,Model是事件的接收者和数据的发起者,Presenter只是起个中转作用。

好,现在我们知道了MVP除了具有MVC的职责分离优点,还能解耦合。接下来我们来看自动化的MVP-MVVM

MVVM

MVVM就是: Model,View,ViewModel。

MVVM的核心是观察者模式,MVVM已经不再职责分离了,当然也没解耦合,他的特点就是响应式。什么意思呢,就是说: 我这边数据变了,你那边立刻知道,不需要经过谁来通知。

MVVM

官方的图是这样的,View和Modle无关联,但是这是不严谨的,因为对于MVVM来说,Model数据变了后,需要通知到View,那么肯定需要直接或间接持有View的引用,所以这个图是不严谨的。

我们将上述MVP的代码改写为MVVM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
typescript复制代码class View {
ViewModel vm;

// 订阅Model的数据
private void init(){
vm.getModel.observer(content, new Observer() {
public void onInfo(String info) {
// 6 接收到数据并展示
showInfo(info);
}
})
}

// 1 发起事件
private void click(){
vm.getInfo();
}

// 7 展示数据
private void showInfo(String info){
setInfo(info);
}
}

class ViewModel {

Model model;

Model getModel(){
return model;
}

// 2 传递事件
public void getInfo(){
model.getIndo();
}
}

class Model {

// 定义观察者,这里已经持有观察者了,也就是持有View了。
List<Observer> observers;

// 添加观察者
public void observe(Observer observe){
observers.add(observe);
}

// 3 接收并处理事件
public void getInfo(){
// 4 获取数据
String info = getInfoFromServer();
// 5 通知数据改动
for(Observer observer : observers) {
observer.onInfo(info);
}
}

// 模拟从服务器获取数据
private String getInfoFromServer(){
String info = "this is info";
return info;
}
}

经过上面伪代码,我们发现ViewModel只传递了事件,不再传递数据了,数据是直接由Model通知到View的,所以我们的:

事件流向: View.click() -> ViewModel.getInfo() -> Model.getInfo() -> 获取数据。

数据流向: Model.getInfo() -> View.Observer.onInfo(info) -> View.showInfo() -> 展示数据。

这里有个很egg pain的点,就是Model间接持有View,第一就是会导致耦合,第二就是可能发生内存泄漏,我们知道Model的生命周期是大于View的,所以要在View消失的时候去反注册掉。当然,使用LiveData可以更好的解决问题。

那么,MVVM的优点在哪呢?就是你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP。

总结

  • MVC更像是一种思想,它描述一种职责分离的思想
  • MVP是MVC的一种表现,它出了具备职责分离,还具备解耦合
  • MVVM是自动化的MVVM,它具备职责分离,具备松耦合,同时还能自动响应数据。

如果你的业务是少量的重逻辑,建议使用MVP(debug方便);如果你的业务是大量的轻逻辑,最好使用MVVM(自动响应数据方便)。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flow 操作符 shareIn 和 stateIn 使用须

发表于 2021-08-19

Flow.shareIn 与 Flow.stateIn 操作符可以将冷流转换为热流: 它们可以将来自上游冷数据流的信息广播给多个收集者。这两个操作符通常用于提升性能: 在没有收集者时加入缓冲;或者干脆作为一种缓存机制使用。

注意 : 冷流 是按需创建的,并且会在它们被观察时发送数据;热流 则总是活跃,无论是否被观察,它们都能发送数据。

本文将会通过示例帮您熟悉 shareIn 与 stateIn 操作符。您将学到如何针对特定用例配置它们,并避免可能遇到的常见陷阱。

底层数据流生产者

继续使用我 之前文章 中使用过的例子——使用底层数据流生产者发出位置更新。它是一个使用 callbackFlow 实现的 冷流。每个新的收集者都会触发数据流的生产者代码块,同时也会将新的回调加入到 FusedLocationProviderClient。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Kotlin复制代码class LocationDataSource(
private val locationClient: FusedLocationProviderClient
) {
val locationsSource: Flow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// 在 Flow 结束收集时进行清理
awaitClose {
removeLocationUpdates(callback)
}
}
}

让我们看看在不同的用例下如何使用 shareIn 与 stateIn 优化 locationsSource 数据流。

shareIn 还是 stateIn?

我们要讨论的第一个话题是 shareIn 与 stateIn 之间的区别。shareIn 操作符返回的是 SharedFlow 而 stateIn 返回的是 StateFlow。

注意 : 要了解有关 StateFlow 与 SharedFlow 的更多信息,可以查看 我们的文档 。

StateFlow 是 SharedFlow 的一种特殊配置,旨在优化分享状态: 最后被发送的项目会重新发送给新的收集者,并且这些项目会使用 Any.equals 进行合并。您可以在 StateFlow 文档 中查看更多相关信息。

两者之间的最主要区别,在于 StateFlow 接口允许您通过读取 value 属性同步访问其最后发出的值。而这不是 SharedFlow 的使用方式。

提升性能

通过共享所有收集者要观察的同一数据流实例 (而不是按需创建同一个数据流的新实例),这些 API 可以为我们提升性能。

在下面的例子中,LocationRepository 消费了 LocationDataSource 暴露的 locationsSource 数据流,同时使用了 shareIn 操作符,从而让每个对用户位置信息感兴趣的收集者都从同一数据流实例中收集数据。这里只创建了一个 locationsSource 数据流实例并由所有收集者共享:

1
2
3
4
5
6
7
Kotlin复制代码class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.shareIn(externalScope, WhileSubscribed())
}

WhileSubscribed 共享策略用于在没有收集者时取消上游数据流。这样一来,我们便能在没有程序对位置更新感兴趣时避免资源的浪费。

Android 应用小提醒! 在大部分情况下,您可以使用 WhileSubscribed(5000),当最后一个收集者消失后再保持上游数据流活跃状态 5 秒钟。这样在某些特定情况 (如配置改变) 下可以避免重启上游数据流。当上游数据流的创建成本很高,或者在 ViewModel 中使用这些操作符时,这一技巧尤其有用。

缓冲事件

在下面的例子中,我们的需求有所改变。现在要求我们保持监听位置更新,同时要在应用从后台返回前台时在屏幕上显示最后的 10 个位置:

1
2
3
4
5
6
7
8
Kotlin复制代码class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource
.shareIn(externalScope, SharingStarted.Eagerly, replay = 10)
}

我们将参数 replay 的值设置为 10,来让最后发出的 10 个项目保持在内存中,同时在每次有收集者观察数据流时重新发送这些项目。为了保持内部数据流始终处于活跃状态并发送位置更新,我们使用了共享策略 SharingStarted.Eagerly,这样就算没有收集者,也能一直监听更新。

缓存数据

我们的需求再次发生变化,这次我们不再需要应用处于后台时 持续 监听位置更新。不过,我们需要缓存最后发送的项目,让用户在获取当前位置时能在屏幕上看到一些数据 (即使数据是旧的)。针对这种情况,我们可以使用 stateIn 操作符。

1
2
3
4
5
6
7
Kotlin复制代码class LocationRepository(
private val locationDataSource: LocationDataSource,
private val externalScope: CoroutineScope
) {
val locations: Flow<Location> =
locationDataSource.locationsSource.stateIn(externalScope, WhileSubscribed(), EmptyLocation)
}

Flow.stateIn 可以缓存最后发送的项目,并重放给新的收集者。

注意!不要在每个函数调用时创建新的实例

切勿 在调用某个函数调用返回时,使用 shareIn 或 stateIn 创建新的数据流。这样会在每次函数调用时创建一个新的 SharedFlow 或 StateFlow,而它们将会一直保持在内存中,直到作用域被取消或者在没有任何引用时被垃圾回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Kotlin复制代码class UserRepository(
private val userLocalDataSource: UserLocalDataSource,
private val externalScope: CoroutineScope
) {
// 不要像这样在函数中使用 shareIn 或 stateIn
// 这将在每次调用时创建新的 SharedFlow 或 StateFlow,而它们将不会被复用。
fun getUser(): Flow<User> =
userLocalDataSource.getUser()
.shareIn(externalScope, WhileSubscribed())

// 可以在属性中使用 shareIn 或 stateIn
val user: Flow<User> =
userLocalDataSource.getUser().shareIn(externalScope, WhileSubscribed())
}

需要入参的数据流

需要入参 (如 userId) 的数据流无法简单地使用 shareIn 或 stateIn 共享。以开源项目——Google I/O 的 Android 应用 iosched 为例,您可以在 源码中 看到,从 Firestore 获取用户事件的数据流是通过 callbackFlow 实现的。由于其接收 userId 作为参数,因此无法简单使用 shareIn 或 stateIn 操作符对其进行复用。

1
2
3
4
5
6
7
8
9
10
Kotlin复制代码class UserRepository(
private val userEventsDataSource: FirestoreUserEventDataSource
) {
// 新的收集者会在 Firestore 中注册为新的回调。
// 由于这一函数依赖一个 `userId`,所以在这个函数中
// 数据流无法通过调用 shareIn 或 stateIn 进行复用.
// 这样会导致每次调用函数时,都会创建新的 SharedFlow 或 StateFlow
fun getUserEvents(userId: String): Flow<UserEventsResult> =
userLocalDataSource.getObservableUserEvents(userId)
}

如何优化这一用例取决于您应用的需求:

  • 您是否允许同时从多个用户接收事件?如果答案是肯定的,您可能需要为 SharedFlow 或 StateFlow 实例创建一个 map,并在 subscriptionCount 为 0 时移除引用并退出上游数据流。
  • 如果您只允许一个用户,并且收集者需要更新为观察新的用户,您可以向一个所有收集者共用的 SharedFlow 或 StateFlow 发送事件更新,并将公共数据流作为类中的变量。

shareIn 与 stateIn 操作符可以与冷流一同使用来提升性能,您可以使用它们在没有收集者时添加缓冲,或者直接将其作为缓存机制使用。小心使用它们,不要在每次函数调用时都创建新的数据流实例——这样会导致资源的浪费及预料之外的问题!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nacos注册中心源码解析(一):服务注册 引言 服务端源码

发表于 2021-08-19

引言

image.png
这是Nacos官方的架构图,可以发现主要的两大模块:ConfigService(配置中心)、NamingService(注册中心),至于什么是配置中心和注册中心这种人尽皆知的问题就不解释了,本篇文章就是站在源码的角度去研究一下nacos的注册流程并且感受一下他所用到的一些值得我们学习的思想。

服务端源码环境搭建

  1. 拉取源码:github.com/alibaba/nac…
  2. 本地启动服务端单机模式需设置参数:-Dnacos.standalone=true

客户端源码分析

Nacos客户端需要导入依赖:

1
2
3
4
xml复制代码<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

注意:在Nacos中如果让客户端主动注册服务器需要导入web启动器

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

下面开始进行源码分析:
首先我们找到nacos-discovery依赖包的启动入口,springboot项目的启动器一般都是以自动配置的方式启动的,所以我们去它的spring.factories文件寻找自动配置类:com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration
image.png
点进去会发现创建了一个非常重要的Bean:

1
2
3
4
5
6
7
8
9
java复制代码@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
NacosServiceRegistry registry,
AutoServiceRegistrationProperties autoServiceRegistrationProperties,
NacosRegistration registration) {
return new NacosAutoServiceRegistration(registry,
autoServiceRegistrationProperties, registration);
}

想要知道这个类是干什么的,首先看一下它的类图:
image.png
继承了ApplicationListener说明他可以进行事件发布,那么我们找一下onApplicationEvent方法看看做了什么事情,跳过套娃的bind()方法,直接找到最核心的方法:start():
image.png
图中1和3的代码都是在注册前后分别发布了事件,这也是nacos客户端的一个扩展点,我们可以自己去监听这些事件做自己的业务处理;注册的主要逻辑还是在第2步,点进去点到最里面发现它是在register()方法中调用了namingService.registerInstance():
image.png
这个心跳的定时任务记下来,先看一下注册的逻辑,点进去,找到调用服务器接口的那段代码:
image.png
到了这一步,客户端注册就结束了。看到这里不难发现一个问题:在Nacos中,不需要使用@EnableDiscoveryClient就可以实现服务的注册。

服务端源码分析

我们切换到Nacos服务端看一下怎么处理服务注册请求的,请求的接口是/nacos/v1/ns/instance,在这个接口中调用了serviceManager.registerInstance(namespaceId, serviceName, instance);

ServiceManager#registerInstance

看下这个方法做了哪些事情
image.png
做事情的主要就1,2两个步骤,先点进去看下1步,点到最里面调用createServiceIfAbsent方法:

ServiceManager#createServiceIfAbsent

image.png
比较核心的就是getService()和putServiceAndInit()方法

  • getService();image.png image.png)原来是从serviceMap中通过namespaceId获取信息的,目前serviceMap还没有任何数据,所以这里返回null。在getSerivce()方法下面去创建一个Serivce对象,那我们先来看一下这个Service对象的组成结构,下面只贴部分代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码    /**
* 用来检测心跳的定时任务
*/
@JsonIgnore
private ClientBeatCheckTask clientBeatCheckTask = new ClientBeatCheckTask(this);

private String namespaceId;

/**
* 如果一段时间没有发送beat,IP将被删除,默认超时时间为30秒。
*/
private long ipDeleteTimeout = 30 * 1000;

private Map<String, Cluster> clusterMap = new HashMap<>();

里面除了心跳检测的定时任务以外还有一个很重要的clusterMap,此时这个clusterMap是空的。到目前为止,我们发现了有很多疑惑的点:

  1. SerivceMap是干什么的?
  2. 创建出来的这个Service对象又是干什么的?
    这里先做个标记,带着这个问题继续往下走。
  • putServiceAndInit():
    代码再回到putServiceAndInit()方法这里,点进去,核心的代码就这两行:
    image.png
    看putService()这个方法名字应该能猜到一些猫腻,要把Service放到哪里呢?点进去看:
    image.png
    原来是把Serivce对象放到了ServiceMap里面了,也就是说下次我们再调用getSerivice(namespaceId)的时候就可以获取到一个Serivice对象了。再看一下sevice.init()方法:
    image.png
    启动了一个定时任务用来处理心跳检测的,看一下clientBeatCkeckTask对象的run方法:
    image.png
    在这个方法里面主要是循环当前service的每一个临时实例 用当前时间减去最后一次心跳时间 是否大于心跳超时时间来判断心跳是否超时,如果大于这个时间会执行instance.setHealthy(false)将实例的健康状态改为false;但是这个定时任务不会立即执行,会每5秒执行一次:
    image.png

ServiceManager#createEmptyService方法的主线业务已经分析完毕,我们来小小的总结一下他到底做了什么:

  1. 创建一个Serivice对象,内部包含了一个clusterMap。
  2. 将service对象放入到SeriviceMap中,结构为:Map<namespaceId, Map<groupName::serviceName, Service>>。
  3. 开启一个定时任务用来检测实例的心跳是否超时,每5秒执行一次。

ServiceManager#addInstance

从上面的源码分析完之后Serivce对象内部结构还没有真正的初始化完。剩余的逻辑都在addInstance方法中,先剧透一下,看下我在这个方法上加的注释,这样一会也好理解:
image.png
点进去看下这个方法咋实现的:
image.png

addIpAddresses()

主要看一下addIpAddresses()方法里面做了那些事情,一直点到最里面的updateIpAddresses()方法:
image.png
这段代码就是创建一个cluster对象,将cluster对象放到service的clusterMap中。那么再看一下Cluster对象长什么样子:

1
2
3
4
5
java复制代码@JsonIgnore
private Set<Instance> persistentInstances = new HashSet<>();

@JsonIgnore
private Set<Instance> ephemeralInstances = new HashSet<>();

这两个Set非常重要,存放的就是注册上来的实例,persistentInstances是持久实例,ephemeralInstances是临时实例,现在这两个Set还是空的。

consistencyService.put(key, instances)

现在Service也初始化完了,按照正常的逻辑来说就差最后的将注册的这个instance存入到Cluster里面了,看一下下一步怎么做的,点到最里面:
image.png

  • onPut(key, value):image.png)image.png)在这里将instance包装成Datum放到dataStore里面并生成一个Key,这个dataStore相当于一个暂存的点。最后task.offer将这个key和执行的动作包装成一个元组扔到内存队列里面就不管了,直接返回了。这里很神奇啊,不是说要把instance存入Cluster里面吗,怎么搞了个内存队列塞进去了,因为要做异步了。那我们找找在哪里做的,看下Notifier的结构:image.png)发现他是实现Runnable接口的,那说明肯定有实现run方法,去run方法里面找找看能不能发现什么(注意:从现在开始以下代码的执行都是异步的,主线程已经结束了):image.png)从队列中将元组拿出来调用handler方法去处理,下面是handler方法的部分代码:image.png)这里的listener.onChange方法实现类是Service,一直点进去会调用updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key))方法,主要看这行代码:image.png这个方法里面会将已经注册过的实例列表复制一份,将新的实例和老的实例都更新到一个集合中,最终再将这个集合更新到真正的实例列表,是一种写时复制的思想,主要时为了解决并发冲突,在写的过程中,其他线程读到的还是旧数据,等真正写完之后再将数据更新回去。

分析完之后我们看一下注册中心的结构长什么样子:
注册表结构.png

忧劳可以兴国,逸豫可以亡身。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

看了那么多博客,还是不懂 TCC,不妨看看这个案例!

发表于 2021-08-19

@[toc]
还是那句老话,网上关于分布式事务讲解理论比较多,案例比较少,最近松哥想通过几个案例,来和大家把常见的分布式事务解决方案过一遍,前面我和大家分享了 Seata 中的 AT 模式,今天我们来看 TCC 模式。

TCC 模式和松哥前面跟大家演示的 AT 模式有很多相似的地方,也有很多不同的地方,之前读者麻瓜大佬投稿过一篇文章讲 TCC 模式:

  • 分布式事务 TCC 原来是这么来的!

感兴趣的小伙伴也可以先看看。

今天我们还是先来整一个案例,把案例分析完了,大家基本上就明白 TCC 是咋回事了,同时也就明白 TCC 和 AT 之间的差异了。

  1. 上代码

还是 Seata 官方的那个仓库,它里边有 TCC 的案例,不过由于它这个仓库案例较多,需要下载的依赖也较多,所以全部导入会容易导入失败,下面是松哥整理好的案例(去除了不必要的工程),可以直接导入,大家可以在公号后台回复 seata-demo 下载这个案例。

官方给的 TCC 案例是一个经典的转账案例,很多小伙伴第一次接触事务的时候,学的案例就是转账,所以这个业务对于大家来说很好理解。

1.1 业务流程

我先来说一下这个案例的业务逻辑,然后我们再来看代码,他的流程是这样的:

  1. 这个项目分两部分,provider 和 consumer(要是只有一个项目也就不存在分布式事务问题了)。
  2. provider 中提供两个转账相关的接口,一个是负责处理扣除账户余额的接口,另一个则是负责给账户添加金额的接口。在该案例中,这两个项目中由一个 provider 提供,在实际操作中,小伙伴们也可以用两个 provider 来分别提供这两个接口。
  3. provider 提供的接口通过 dubbo 暴露出去,consumer 则通过 dubbo 来引用这些暴露出来的接口。
  4. 转账操作分两步:首先调用 FirstTccAction 从一个账户中减除金额;然后调用 SecondTccAction 给一个账户增加金额。两个操作要么同时成功,要么同时失败。

有人可能会说,都是 provider 提供的接口,也算分布式事务?算!当然算!虽然上面提到的两个接口都是 provider 提供的,但是由于这里存在两个数据库,不同接口操作不同的数据库,所以依然是分布式事务。

这是这个项目大致上要做的事情。

1.2 案例配置

官方的案例用的是 H2 数据库,这个大家不方便看效果,因此,我们这里稍微做一点配置,将数据库换为 MySQL,这样我们方便看转账效果。

具体配置步骤如下:

  1. 首先在本地 MySQL 中创建两个数据库:

创建两个空的库就行了,不用创建表,项目启动的时候会自动初始化表。

  • transfer_from_db:转出账户的库。
  • transfer_to_db:转入账户的库。
  1. 修改项目的数据库连接池版本。

官方给的案例有点小问题,直接启动会报错,原因在于案例中使用的 DBCP 和 MyBatis 版本冲突,需要大家先在 pom.xml 中把 DBCP 的版本号改为 1.4,如下:

1
2
3
4
5
6
7
xml复制代码<properties>
<curator.version>4.2.0</curator.version>
<commons-dbcp.version>1.4</commons-dbcp.version>
<h2.version>1.4.181</h2.version>
<mybatis.version>3.5.6</mybatis.version>
<mybatis.spring.version>1.3.1</mybatis.spring.version>
</properties>

然后我们再加入 MySQL 驱动,如下:

1
2
3
4
5
xml复制代码<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>

虽然案例中有的东西有点像老古董了,但是本着能简则简的原则,我就不去修改了,咱们只要项目跑起来,能够帮助我们理解 TCC 就行了。

另外,这个项目引用的 Dubbo 版本也有问题,我们手动给其加上版本号(默认的 3.0.1 这个版本有问题,松哥亲测 2.7.3 可用):

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
</exclusions>
<version>2.7.3</version>
</dependency>
  1. 修改数据库配置。

数据库配置有两个,一个是转账转出数据源,另一个是转账转入数据源,相关配置在 src/main/resources/db-bean 目录下。

先来修改 from-datasource-bean.xml,主要修改数据源,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<bean id="fromAccountDataSource"  class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName">
<value>com.mysql.cj.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql:///transfer_from_db?serverTimezone=Asia/Shanghai</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value>123</value>
</property>
</bean>

改四个东西:数据库驱动、数据库连接地址、数据库用户名、数据库密码。

再来修改 to-datasource-bean.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<bean id="toAccountDataSource"  class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName">
<value>com.mysql.cj.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql:///transfer_to_db?serverTimezone=Asia/Shanghai</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value>123</value>
</property>
</bean>

这两个配置主要是连接的数据库不同。

OK,如此之后,我们的配置就算完成了。

1.3 案例运行

案例运行分为两部分。

1.3.1 启动 Provider

找到 src/main/java/io/seata/samples/tcc/transfer/starter/TransferProviderStarter.java,执行 main 方法,直接执行即可,执行之后,控制台看到如下信息就表示项目启动成功并且表结构以及表数据初始化成功:

启动过程中,可能会有一个空指针异常,不过并不影响使用,所以可以忽略之。

项目启动成功之后,我们可以查看一下刚刚创建好的两个数据库,每个数据库里边都有三张表:

先来看转出的库:

account 表中有两条记录:

这张表中有 A、B 两个账户,各有 100 块钱,各自被冻结的资金(freezed_amount)都为 0。

business_action 和 business_activity 都是空表。

再来看转入的库:

可以看到,和 transfer_from_db 一模一样的三张表,就是 account 中的用户是 C,也有 100 块钱。

1.3.2 开启转账逻辑

找到 src/main/java/io/seata/samples/tcc/transfer/starter/TransferApplication.java ,这个里边的 main 方法中有两个测试方法,doTransferSuccess 会转账成功,doTransferFailed 则会转账失败。

这两个方法我们首先注释掉 doTransferFailed,运行 doTransferSuccess 方法,控制台输出日志如下:

这表示转账成功。

此时查看数据库,A 账户少了 10 块钱,C 账户多了 10 块钱:


然后我们注释掉 doTransferSuccess ,运行 doTransferFailed 方法,结果如下:

可以看到,转账失败,此时查看数据库,发现两个库中的数据均未发生改变,说明数据已经回滚了。

好啦,这就是官方给我们提供的一个典型的转账案例。那么这个转账案例是怎么实现的?接下来我们来分析一下代码,代码分析完了,大家就明白什么是 TCC 了!

  1. 代码分析

这里关于 Dubbo 的调用逻辑,松哥就不多说了,相信大家都会,咱们主要来说说跟分布式事务相关的代码。

首先,这个项目中提供了两个接口:

  • FirstTccAction
  • SecondTccAction

这两个接口分别代表了转账时候的两个步骤:

  • FirstTccAction:这个接口中用来处理转出账户余额问题(减钱),这个接口中使用的数据源就是 transfer_from_db。
  • SecondTccAction:这个接口用来处理转入账户问题(加钱),这个接口中使用的数据源就是 transfer_to_db。

这两个接口的定义其实非常类似,只要我们看懂其中一个,另外一个就很容易懂了。

2.1 FirstTccAction

这是把钱转出去的接口,我们先来看接口的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public interface FirstTccAction {

/**
* 一阶段方法
*
* @param businessActionContext
* @param accountNo
* @param amount
*/
@TwoPhaseBusinessAction(name = "firstTccAction", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepareMinus(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "accountNo") String accountNo,
@BusinessActionContextParameter(paramName = "amount") double amount);

/**
* 二阶段提交
* @param businessActionContext
* @return
*/
public boolean commit(BusinessActionContext businessActionContext);

/**
* 二阶段回滚
* @param businessActionContext
* @return
*/
public boolean rollback(BusinessActionContext businessActionContext);
}

可以看到,接口中有三个方法:

  1. prepareMinus
  2. commit
  3. rollback

这三个方法的名字并不是固定的,可以自己定义,我们来看这三个方法是干嘛的(实现类是 FirstTccActionImpl):

  1. prepareMinus:这个方法看名字就知道可以在该方法中做准备工作,转账的准备工作都是什么呢?检查账户是否存在、冻结转账资金等等操作都可以在这个方法中完成。以上面的案例为例(A 账户转账 10 块钱到 C 账户),具体来说,在 FirstTccActionImpl#prepareMinus 方法中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码@Override
public boolean prepareMinus(BusinessActionContext businessActionContext, final String accountNo, final double amount) {
//分布式事务ID
final String xid = businessActionContext.getXid();
return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>(){
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
//校验账户余额
Account account = fromAccountDAO.getAccountForUpdate(accountNo);
if(account == null){
throw new RuntimeException("账户不存在");
}
if (account.getAmount() - amount < 0) {
throw new RuntimeException("余额不足");
}
//冻结转账金额
double freezedAmount = account.getFreezedAmount() + amount;
account.setFreezedAmount(freezedAmount);
fromAccountDAO.updateFreezedAmount(account);
System.out.println(String.format("prepareMinus account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
return true;
} catch (Throwable t) {
t.printStackTrace();
status.setRollbackOnly();
return false;
}
}
});
}

这个方法就干了三件事:1.检查 A 账户是否存在,不存在就抛异常;2.检查 A 账户余额是否小于 10 块钱,如果是,抛异常(钱不够,没法转账);3.修改 A 账户的数据库记录,将冻结资金标记出来(A 账户的 freezed_amount 字段将被修改为 10)。

  1. prepareMinus 方法所做的事情都属于一阶段的事情。
  2. prepareMinus 方法有一个 @TwoPhaseBusinessAction 注解,用来标记事务,该注解中,commitMethod 注解表示事务提交的方法,rollbackMethod 表示事务回滚的方法,这两个方法都是该事务中定义的方法。
  3. prepareMinus 方法是由开发者自己调用,因此可以自定义参数传进来,而 commit 和 rollback 方法则是由框架来调用(如果一阶段出问题了,二阶段自动回滚;一阶段没问题,二阶段就自动提交),但是在框架调用的时候,我们可能还是需要一些业务相关的参数,所以在 prepareMinus 方法中,我们可以通过 @BusinessActionContextParameter 注解来把在 commit 以及 rollback 中需要的参数绑定到 BusinessActionContext 中,将来在 commit 和 rollback 方法中就可以获取到这些参数。
  4. commit 方法是二阶段提交的方法,如果一阶段的工作都顺利进行完了,则进行二阶段的事务提交。具体实现在 FirstTccActionImpl#commit 方法中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@Override
public boolean commit(BusinessActionContext businessActionContext) {
//分布式事务ID
final String xid = businessActionContext.getXid();
//账户ID
final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
//转出金额
final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try{
Account account = fromAccountDAO.getAccountForUpdate(accountNo);
//扣除账户余额
double newAmount = account.getAmount() - amount;
if (newAmount < 0) {
throw new RuntimeException("余额不足");
}
account.setAmount(newAmount);
//释放账户 冻结金额
account.setFreezedAmount(account.getFreezedAmount() - amount);
fromAccountDAO.updateAmount(account);
System.out.println(String.format("minus account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
return true;
}catch (Throwable t){
t.printStackTrace();
status.setRollbackOnly();
return false;
}
}
});
}

看看这个方法的执行逻辑:

  • 首先从 BusinessActionContext 对象中把 prepareMinus 中的那几个参数拎出来。
  • 然后判断一下账户余额是否充足(是否够转账)。
  • 更新账户余额和冻结的金额(余额正常转账,冻结的金额归零)。

这就是 commit 方法所做的事情。

  1. rollback 方法是二阶段的回滚方法,如果一阶段的方法执行出问题了,二阶段就要回滚,回滚要做的事情就是反向补偿操作,具体实现在 FirstTccActionImpl#rollback 方法中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码@Override
public boolean rollback(BusinessActionContext businessActionContext) {
//分布式事务ID
final String xid = businessActionContext.getXid();
//账户ID
final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
//转出金额
final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try{
Account account = fromAccountDAO.getAccountForUpdate(accountNo);
if(account == null){
//账户不存在,回滚什么都不做
return true;
}
//释放冻结金额
account.setFreezedAmount(account.getFreezedAmount() - amount);
fromAccountDAO.updateFreezedAmount(account);
System.out.println(String.format("Undo prepareMinus account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
return true;
}catch (Throwable t){
t.printStackTrace();
status.setRollbackOnly();
return false;
}
}
});
}

可以看到,回滚的反向补偿其实很简单,先看下账户是否存在,账户存在的话,把冻结的资金取消冻结就行了。

这就是把钱转出去的整个过程。

2.2 SecondTccAction

这是把钱转进来的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码
public interface SecondTccAction {

/**
* 一阶段方法
*
* @param businessActionContext
* @param accountNo
* @param amount
*/
@TwoPhaseBusinessAction(name = "secondTccAction", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepareAdd(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "accountNo") String accountNo,
@BusinessActionContextParameter(paramName = "amount") double amount);

/**
* 二阶段提交
* @param businessActionContext
* @return
*/
public boolean commit(BusinessActionContext businessActionContext);

/**
* 二阶段回滚
* @param businessActionContext
* @return
*/
public boolean rollback(BusinessActionContext businessActionContext);

}

接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
java复制代码public class SecondTccActionImpl implements SecondTccAction {

/**
* 加钱账户 DAP
*/
private AccountDAO toAccountDAO;

private TransactionTemplate toDsTransactionTemplate;

/**
* 一阶段准备,转入资金 准备
* @param businessActionContext
* @param accountNo
* @param amount
* @return
*/
@Override
public boolean prepareAdd(final BusinessActionContext businessActionContext, final String accountNo, final double amount) {
//分布式事务ID
final String xid = businessActionContext.getXid();

return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>(){

@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
//校验账户
Account account = toAccountDAO.getAccountForUpdate(accountNo);
if(account == null){
System.out.println("prepareAdd: 账户["+accountNo+"]不存在, txId:" + businessActionContext.getXid());
return false;
}
//待转入资金作为 不可用金额
double freezedAmount = account.getFreezedAmount() + amount;
account.setFreezedAmount(freezedAmount);
toAccountDAO.updateFreezedAmount(account);
System.out.println(String.format("prepareAdd account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
return true;
} catch (Throwable t) {
t.printStackTrace();
status.setRollbackOnly();
return false;
}
}
});
}

/**
* 二阶段提交
* @param businessActionContext
* @return
*/
@Override
public boolean commit(BusinessActionContext businessActionContext) {
//分布式事务ID
final String xid = businessActionContext.getXid();
//账户ID
final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
//转出金额
final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {

@Override
public Boolean doInTransaction(TransactionStatus status) {
try{
Account account = toAccountDAO.getAccountForUpdate(accountNo);
//加钱
double newAmount = account.getAmount() + amount;
account.setAmount(newAmount);
//冻结金额 清除
account.setFreezedAmount(account.getFreezedAmount() - amount);
toAccountDAO.updateAmount(account);

System.out.println(String.format("add account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
return true;
}catch (Throwable t){
t.printStackTrace();
status.setRollbackOnly();
return false;
}
}
});

}

/**
* 二阶段回滚
* @param businessActionContext
* @return
*/
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
//分布式事务ID
final String xid = businessActionContext.getXid();
//账户ID
final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
//转出金额
final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {

@Override
public Boolean doInTransaction(TransactionStatus status) {
try{
Account account = toAccountDAO.getAccountForUpdate(accountNo);
if(account == null){
//账户不存在, 无需回滚动作
return true;
}
//冻结金额 清除
account.setFreezedAmount(account.getFreezedAmount() - amount);
toAccountDAO.updateFreezedAmount(account);

System.out.println(String.format("Undo prepareAdd account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
return true;
}catch (Throwable t){
t.printStackTrace();
status.setRollbackOnly();
return false;
}
}
});
}
}

看懂了上面的 FirstTccActionImpl,SecondTccActionImpl 这个接口松哥就不啰嗦了,简单说一下:

  1. 在 prepareAdd 方法中,判断转入账户是否存在,如果存在的话,就把转入资金先存入冻结的那个字段中(不是直接加到账户余额上)。
  2. 在 commit 方法中,事务提交的时候,把冻结的资金加入到账户余额中,同时清除冻结金额。
  3. 在 rollback 方法中,事务回滚的时候,反向补偿把冻结的资金清除即可。

这就是把钱收进来的大致过程。

2.3 TransferServiceImpl

具体转账是在 TransferServiceImpl 类中,在它的 transfer 方法中,去调用 FirstTccAction 和 SecondTccAction,一起来看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码public class TransferServiceImpl implements TransferService {

private FirstTccAction firstTccAction;

private SecondTccAction secondTccAction;

/**
* 转账操作
* @param from 扣钱账户
* @param to 加钱账户
* @param amount 转账金额
* @return
*/
@Override
@GlobalTransactional
public boolean transfer(final String from, final String to, final double amount) {
//扣钱参与者,一阶段执行
boolean ret = firstTccAction.prepareMinus(null, from, amount);

if(!ret){
//扣钱参与者,一阶段失败; 回滚本地事务和分布式事务
throw new RuntimeException("账号:["+from+"] 预扣款失败");
}

//加钱参与者,一阶段执行
ret = secondTccAction.prepareAdd(null, to, amount);

if(!ret){
throw new RuntimeException("账号:["+to+"] 预收款失败");
}

System.out.println(String.format("transfer amount[%s] from [%s] to [%s] finish.", String.valueOf(amount), from, to));
return true;
}

public void setFirstTccAction(FirstTccAction firstTccAction) {
this.firstTccAction = firstTccAction;
}

public void setSecondTccAction(SecondTccAction secondTccAction) {
this.secondTccAction = secondTccAction;
}
}

来看一下具体的转账逻辑:

  1. 首先注入刚刚的 FirstTccAction 和 SecondTccAction,如果这是一个微服务项目,那就在这里把各自的 Feign 搞进来。
  2. transfer 方法就执行具体的转账逻辑,该方法加上 @GlobalTransactional 注解。这个方法中主要是去调用 prepareXXX 完成一阶段的事情,如果一阶段出问题了,那么就会抛出异常,则事务会回滚(二阶段),回滚就会自动调用 FirstTccAction 和 SecondTccAction 各自的 rollback 方法(反向补偿);如果一阶段执行没问题,则二阶段就调用 FirstTccAction 和 SecondTccAction 的 commit 方法,完成提交。

这就是大致的转账逻辑。

  1. TCC Vs AT

经过上面的分析,相信小伙伴们对 TCC 已经有一些感觉了。

那么什么是 TCC?

TCC 是 Try-Confirm-Cancel 英文单词的简写。

在 TCC 模式中,一个事物是通过 Do-Commit/Rollback 来实现的,开发者需要给每一个服务间调用的操作接口,都提供一套 Try-Confirm/Cancel 接口,这套接口就类似于我们上面的 prepareXXX/commit/rollback 接口。

再举一个简化的电商案例,用户支付完成的时候由先订单服务处理,然后调用商品服务去减库存,这两个操作同时成功或者同时失败,这就涉及到分布式事务了:在 TCC 模式下,我们需要 3 个接口。首先是减库存的 Try 接口,在这里,我们要检查业务数据的状态、检查商品库存够不够,然后做资源的预留,也就是在某个字段上设置预留的状态,然后在 Confirm 接口里,完成库存减 1 的操作,在 Cancel 接口里,把之前预留的字段重置(预留的状态其实就类似于前面案例的冻结资金字段 freezed_amount)。

为什么搞得这么麻烦呢?分成三个步骤来做有一个好处,就是在出错的时候,能够顺利的完成数据库重置(反向补偿),并且,只要我们 prepare 中的逻辑是正确的,那么即使 confirm 执行出错了,我们也可以进行重试。

我们再来看下面一张图:

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode。

AT 模式基于支持本地 ACID 事务的关系型数据库:

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

关于 AT 这块,如果小伙伴们不熟悉,可以参考松哥前面的文章:

  • 五分钟带你体验一把分布式事务!so easy!

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
  • 二阶段 commit 行为:调用自定义的 commit 逻辑。
  • 二阶段 rollback 行为:调用自定义的 rollback 逻辑。

所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

回顾前面的案例,小伙伴们发现,分布式事务两阶段提交,在 TCC 中,prepare、commit 以及 rollback 中的逻辑都是我们自己写的,因此说 TCC 不依赖于底层数据资源的事务支持。

相比于 AT 模式,TCC 需要我们自己实现 prepare、commit 以及 rollback 逻辑,而在 AT 模式中,commit 和 rollback 都不用我们去管,Seata 会自动帮我们完成。

  1. 小结

好啦,今天这篇文章松哥就和大家简单分享一下 Seata 中的 TCC 模式,建议小伙伴们一定先跑一下文章中的案例,然后再去看分析,就很容易懂了~

分布式事务的其他解决方案,我们后面再继续聊~

公众号江南一点雨后台回复 seata-demo,可以下载本文案例。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…557558559…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%