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

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


  • 首页

  • 归档

  • 搜索

深度剖析synchronized实现原理

发表于 2021-06-03

synchronized是面试中的高频考点,看了这篇文章再也不用担心面试官问synchronized了

推荐阅读:

  • MySQL数据库高频面试题
  • 计算机网络高频面试题最新版
  • Java集合高频面试题最新版
  • 并发编程高频面试题
  • 面试官:请用五种方法实现多线程交替打印问题

文章目录:

目录.png

什么是synchronized关键字?

在多线程的环境下,多个线程同时访问共享资源会出现一些问题,而synchronized关键字则是用来保证线程同步的。

Java内存的可见性问题

在了解synchronized关键字的底层原理前,需要先简单了解下Java的内存模型,看看synchronized关键字是如何起作用的。

在这里插入图片描述

这里的本地内存并不是真实存在的,只是Java内存模型的一个抽象概念,它包含了控制器、运算器、缓存等。同时Java内存模型规定,线程对共享变量的操作必须在自己的本地内存中进行,不能直接在主内存中操作共享变量。这种内存模型会出现什么问题呢?,

  1. 线程A获取到共享变量X的值,此时本地内存A中没有X的值,所以加载主内存中的X值并缓存到本地内存A中,线程A修改X的值为1,并将X的值刷新到主内存中,这时主内存及本地内存中的X的值都为1。
  2. 线程B需要获取共享变量X的值,此时本地内存B中没有X的值,加载主内存中的X值并缓存到本地内存B中,此时X的值为1。线程B修改X的值为2,并刷新到主内存中,此时主内存及本地内存B中的X值为2,本地内存A中的X值为1。
  3. 线程A再次获取共享变量X的值,此时本地内存中存在X的值,所以直接从本地内存中A获取到了X为1的值,但此时主内存中X的值为2,到此出现了所谓内存不可见的问题。

该问题Java内存模型是通过synchronized关键字和volatile关键字就可以解决,那么synchronized关键字是如何解决的呢,其实进入synchronized块就是把在synchronized块内使用到的变量从线程的本地内存中擦除,这样在synchronized块中再次使用到该变量就不能从本地内存中获取了,需要从主内存中获取,解决了内存不可见问题。

synchronized关键字三大特性是什么?

面试时经常拿synchronized关键字和volatile关键字的特性进行对比,synchronized关键字可以保证并发编程的三大特性:原子性、可见性、有序性,而volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的synchronized。

  • 原子性:一个或多个操作要么全部执行成功,要么全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源。
  • 可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。执行synchronized时,会对应执行 lock 、unlock原子操作,保证可见性。
  • 有序性:程序的执行顺序会按照代码的先后顺序执行。

synchronized关键字可以实现什么类型的锁?

  • 悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。
  • 非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。
  • 可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。
  • 独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。

synchronized关键字的使用方式

synchronized主要有三种使用方式:修饰普通同步方法、修饰静态同步方法、修饰同步方法块。

修饰普通同步方法(实例方法)

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
java复制代码class syncTest implements Runnable {

private static int i = 0; //共享资源

private synchronized void add() {
i++;
}

@Override
public void run() {
for (int j = 0; j < 10000; j++) {
add();
}
}

public static void main(String[] args) throws Exception {

syncTest syncTest = new syncTest();

Thread t1 = new Thread(syncTest);
Thread t2 = new Thread(syncTest);

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println(i);
}
}

这是一个非常经典的例子,多个线程操作i++会出现线程不安全问题,这段代码的结果很容易得到

1
java复制代码20000

大家可以再看看这段代码,猜一猜它的运行结果

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
java复制代码class syncTest implements Runnable {

private static int i = 0; //共享资源

private synchronized void add() {
i++;
}

@Override
public void run() {
for (int j = 0; j < 10000; j++) {
add();
}
}

public static void main(String[] args) throws Exception {

// syncTest syncTest = new syncTest();

Thread t1 = new Thread(new syncTest());
Thread t2 = new Thread(new syncTest());

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println(i);
}
}

结果为

1
java复制代码18634

第二个示例中的add()方法虽然也使用synchronized关键字修饰了,但是因为两次new syncTest()操作建立的是两个不同的对象,也就是说存在两个不同的对象锁,线程t1和t2使用的是不同的对象锁,所以不能保证线程安全。那这种情况应该如何解决呢?因为每次创建的实例对象都是不同的,而类对象却只有一个,如果synchronized关键字作用于类对象,即用synchronized修饰静态方法,问题则迎刃而解。

修饰静态方法

只需要在add()方法前用static修饰即可,即当synchronized作用于静态方法,锁就是当前的class对象。

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
java复制代码class syncTest implements Runnable {

private static int i = 0; //共享资源

private static synchronized void add() {
i++;
}

@Override
public void run() {
for (int j = 0; j < 10000; j++) {
add();
}
}

public static void main(String[] args) throws Exception {

// syncTest syncTest = new syncTest();

Thread t1 = new Thread(new syncTest());
Thread t2 = new Thread(new syncTest());

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println(i);
}
}

结果为

1
java复制代码20000

修饰同步代码代码块

如果某些情况下,整个方法体比较大,需要同步的代码只是一小部分,如果直接对整个方法体进行同步,会使得代码性能变差,这时只需要对一小部分代码进行同步即可。代码如下:

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
java复制代码class syncTest implements Runnable {

static int i = 0; //共享资源

@Override
public void run() {
//其他操作.......
synchronized (this){ //this表示当前对象实例,这里还可以使用syncTest.class,表示class对象锁
for (int j = 0; j < 10000; j++) {
i++;
}
}

}

public static void main(String[] args) throws Exception {

syncTest syncTest = new syncTest();

Thread t1 = new Thread(syncTest);
Thread t2 = new Thread(syncTest);

t1.start();
t2.start();

t1.join();
t2.join();

System.out.println(i);
}
}

输出结果:

1
java复制代码20000

synchronized关键字的底层原理

这个问题也是面试比较高频的一个问题,也是比较难理解的,理解synchronized需要一定的Java虚拟机的知识。

在jdk1.6之前,synchronized被称为重量级锁,在jdk1.6中,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁和轻量级锁。下面先介绍jdk1.6之前的synchronized原理。

对象头

在HotSpot虚拟机中,Java对象在内存中的布局大致可以分为三部分:对象头、实例数据和填充对齐。因为synchronized用的锁是存在对象头里的,这里我们需要重点了解对象头。如果对象头是数组类型,则对象头由Mark Word、Class MetadataAddress和Array length组成,如果对象头非数组类型,对象头则由Mark Word和Class MetadataAddress组成。在32位虚拟机中,数组类型的Java对象头的组成如下表:

内容 说明 长度
Mark Word 存储对象的hashCode、分代年龄和锁标记位 32bit
Class MetadataAddress 存储到对象类型数据的指针 32bit
Array length 数组的长度 32bit

这里我们需要重点掌握的是Mark Word。

Mark Word

在运行期间,Mark Word中存储的数据会随着锁标志位的变化而变化,在32位虚拟机中,不同状态下的组成如下:

在这里插入图片描述

其中线程ID表示持有偏向锁线程的ID,Epoch表示偏向锁的时间戳,偏向锁和轻量级锁是在jdk1.6中引入的。

重量级锁的底部实现原理:Monitor

在jdk1.6之前,synchronized只能实现重量级锁,Java虚拟机是基于Monitor对象来实现重量级锁的,所以首先来了解下Monitor,在Hotspot虚拟机中,Monitor是由ObjectMonitor实现的,其源码是用C++语言编写的,首先我们先下载Hotspot的源码,源码下载链接:hg.openjdk.java.net/jdk8/jdk8/h…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C++复制代码ObjectMonitor() {
_header = NULL;
_count = 0; //锁的计数器,获取锁时count数值加1,释放锁时count值减1,直到
_waiters = 0, //等待线程数
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //指向持有ObjectMonitor对象的线程地址
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //阻塞在EntryList上的单向线程列表
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

其中 _owner、_WaitSet和_EntryList 字段比较重要,它们之间的转换关系如下图

在这里插入图片描述

从上图可以总结获取Monitor和释放Monitor的流程如下:

  1. 当多个线程同时访问同步代码块时,首先会进入到EntryList中,然后通过CAS的方式尝试将Monitor中的owner字段设置为当前线程,同时count加1,若发现之前的owner的值就是指向当前线程的,recursions也需要加1。如果CAS尝试获取锁失败,则进入到EntryList中。
  2. 当获取锁的线程调用wait()方法,则会将owner设置为null,同时count减1,recursions减1,当前线程加入到WaitSet中,等待被唤醒。
  3. 当前线程执行完同步代码块时,则会释放锁,count减1,recursions减1。当recursions的值为0时,说明线程已经释放了锁。

之前提到过一个常见面试题,为什么wait()、notify()等方法要在同步方法或同步代码块中来执行呢,这里就能找到原因,是因为wait()、notify()方法需要借助ObjectMonitor对象内部方法来完成。

synchronized作用于同步代码块的实现原理

前面已经了解Monitor的实现细节,而Java虚拟机则是通过进入和退出Monitor对象来实现方法同步和代码块同步的。这里为了更方便看程序字节码执行指令,我先在IDEA中安装了一个jclasslib Bytecode viewer插件。我们先来看这个synchronized作用于同步代码块的代码。

1
2
3
4
5
6
7
8
9
java复制代码    public void run() {
//其他操作.......
synchronized (this){ //this表示当前对象实例,这里还可以使用syncTest.class,表示class对象锁
for (int j = 0; j < 10000; j++) {
i++;
}
}

}

查看代码字节码指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码 1 dup
2 astore_1
3 monitorenter //进入同步代码块的指令
4 iconst_0
5 istore_2
6 iload_2
7 sipush 10000
10 if_icmpge 27 (+17)
13 getstatic #2 <com/company/syncTest.i>
16 iconst_1
17 iadd
18 putstatic #2 <com/company/syncTest.i>
21 iinc 2 by 1
24 goto 6 (-18)
27 aload_1
28 monitorexit //结束同步代码块的指令
29 goto 37 (+8)
32 astore_3
33 aload_1
34 monitorexit //遇到异常时执行的指令
35 aload_3
36 athrow
37 return

从上述字节码中可以看到同步代码块的实现是由monitorenter 和monitorexit指令完成的,其中monitorenter指令所在的位置是同步代码块开始的位置,第一个monitorexit 指令是用于正常结束同步代码块的指令,第二个monitorexit 指令是用于异常结束时所执行的释放Monitor指令。

synchronized作用于同步方法原理

1
2
3
java复制代码    private synchronized void add() {
i++;
}

查看字节码如下:

1
2
3
4
5
java复制代码0 getstatic #2 <com/company/syncTest.i>
3 iconst_1
4 iadd
5 putstatic #2 <com/company/syncTest.i>
8 return

发现这个没有monitorenter 和 monitorexit 这两个指令了,而在查看该方法的class文件的结构信息时发现了Access flags后边的synchronized标识,该标识表明了该方法是一个同步方法。Java虚拟机通过该标识可以来辨别一个方法是否为同步方法,如果有该标识,线程将持有Monitor,在执行方法,最后释放Monitor。

在这里插入图片描述

原理大概就是这样,最后总结一下,面试中应该简洁地如何回答synchroized的底层原理这个问题。

答:Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是monitorenter 和 monitorexit 指令实现的,而方法同步是通过Access flags后面的标识来确定该方法是否为同步方法。

Jdk1.6为什么要对synchronized进行优化?

因为Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,而Monitor是依靠底层操作系统的Mutex Lock来实现的,操作系统实现线程之间的切换需要从用户态转换到内核态,这个切换成本比较高,对性能影响较大。

jDK1.6对synchronized做了哪些优化?

锁的升级

在JDK1.6中,为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,锁的状态变成了四种,如下图所示。锁的状态会随着竞争激烈逐渐升级,但通常情况下,锁的状态只能升级不能降级。这种只能升级不能降级的策略是为了提高获得锁和释放锁的效率。

在这里插入图片描述

偏向锁

常见面试题:偏向锁的原理(或偏向锁的获取流程)、偏向锁的好处是什么(获取偏向锁的目的是什么)

引入偏向锁的目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。

偏向锁的获取流程:

  1. 检查对象头中Mark Word是否为可偏向状态,如果不是则直接升级为轻量级锁。
  2. 如果是,判断Mark Work中的线程ID是否指向当前线程,如果是,则执行同步代码块。
  3. 如果不是,则进行CAS操作竞争锁,如果竞争到锁,则将Mark Work中的线程ID设为当前线程ID,执行同步代码块。
  4. 如果竞争失败,升级为轻量级锁。

偏向锁的获取流程如下图:

在这里插入图片描述

偏向锁的撤销:

只有等到竞争,持有偏向锁的线程才会撤销偏向锁。偏向锁撤销后会恢复到无锁或者轻量级锁的状态。

  1. 偏向锁的撤销需要到达全局安全点,全局安全点表示一种状态,该状态下所有线程都处于暂停状态。
  2. 判断锁对象是否处于无锁状态,即获得偏向锁的线程如果已经退出了临界区,表示同步代码已经执行完了。重新竞争锁的线程会进行CAS操作替代原来线程的ThreadID。
  3. 如果获得偏向锁的线程还处于临界区之内,表示同步代码还未执行完,将获得偏向锁的线程升级为轻量级锁。

一句话简单总结偏向锁原理:使用CAS操作将当前线程的ID记录到对象的Mark Word中。

轻量级锁

引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。

轻量级锁的获取流程:

  1. 首先判断当前对象是否处于一个无锁的状态,如果是,Java虚拟机将在当前线程的栈帧建立一个锁记录(Lock Record),用于存储对象目前的Mark Word的拷贝,如图所示。

在这里插入图片描述
2. 将对象的Mark Word复制到栈帧中的Lock Record中,并将Lock Record中的owner指向当前对象,并使用CAS操作将对象的Mark Word更新为指向Lock Record的指针,如图所示。

在这里插入图片描述
3. 如果第二步执行成功,表示该线程获得了这个对象的锁,将对象Mark Word中锁的标志位设置为“00”,执行同步代码块。
4. 如果第二步未执行成功,需要先判断当前对象的Mark Word是否指向当前线程的栈帧,如果是,表示当前线程已经持有了当前对象的锁,这是一次重入,直接执行同步代码块。如果不是表示多个线程存在竞争,该线程通过自旋尝试获得锁,即重复步骤2,自旋超过一定次数,轻量级锁升级为重量级锁。

轻量级锁的解锁:

轻量级的解锁同样是通过CAS操作进行的,线程会通过CAS操作将Lock Record中的Mark Word(官方称为Displaced Mark Word)替换回来。如果成功表示没有竞争发生,成功释放锁,恢复到无锁的状态;如果失败,表示当前锁存在竞争,升级为重量级锁。

一句话总结轻量级锁的原理:将对象的Mark Word复制到当前线程的Lock Record中,并将对象的Mark Word更新为指向Lock Record的指针。

自旋锁

Java锁的几种状态并不包括自旋锁,当轻量级锁的竞争就是采用的自旋锁机制。

什么是自旋锁:当线程A已经获得锁时,线程B再来竞争锁,线程B不会直接被阻塞,而是在原地循环 等待,当线程A释放锁后,线程B可以马上获得锁。

引入自旋锁的原因:因为阻塞和唤起线程都会引起操作系统用户态和核心态的转变,对系统性能影响较大,而自旋等待可以避免线程切换的开销。

自旋锁的缺点:自旋等待虽然可以避免线程切花的开销,但它也会占用处理器的时间。如果持有锁的线程在较短的时间内释放了锁,自旋锁的效果就比较好,如果持有锁的线程很长时间都不释放锁,自旋的线程就会白白浪费资源,所以一般线程自旋的次数必须有一个限制,该次数可以通过参数-XX:PreBlockSpin调整,一般默认为10。

自适应自旋锁:JDK1.6引入了自适应自旋锁,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果对于某个锁对象,刚刚有线程自旋等待成功获取到锁,那么虚拟机将认为这次自旋等待的成功率也很高,会允许线程自旋等待的时间更长一些。如果对于某个锁对象,线程自旋等待很少成功获取到锁,那么虚拟机将会减少线程自旋等待的时间。

偏向锁、轻量级锁、重量级锁的对比

锁 优点 缺点 实用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在竞争,会额外带来锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求响应时间,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较慢

该表格出自《Java并发编程的艺术》

了解锁消除吗?

锁消除是指Java虚拟机在即时编译时,通过对运行上下的扫描,消除那些不可能存在共享资源竞争的锁。锁消除可以节约无意义的请求锁时间。

了解锁粗化吗

一般情况下,为了提高性能,总是将同步块的作用范围限制到最小,这样可以使得需要同步的操作尽可能地少。但如果一系列连续的操作一直对某个对象反复加锁和解锁,频繁地进行互斥同步操作也会引起不必要的性能消耗。

如果虚拟机检测到有一系列操作都是对某个对象反复加锁和解锁,会将加锁同步的范围粗化到整个操作序列的外部。可以看下面这个经典案例。

1
java复制代码for(int i=0;i<n;i++){    synchronized(lock){    }}

这段代码会导致频繁地加锁和解锁,锁粗化后

1
java复制代码synchronized(lock){    for(int i=0;i<n;i++){    }}

当线程1进入到一个对象的synchronized方法A后,线程2是否可以进入到此对象的synchronized方法B?

不能,线程2只能访问该对象的非同步方法。因为执行同步方法时需要获得对象的锁,而线程1在进入sychronized修饰的方A时已经获取到了锁,线程2只能等待,无法进入到synchronized修饰的方法B,但可以进入到其他非synchronized修饰的方法。

synchronized和volatile的区别?

  • volatile主要是保证内存的可见性,即变量在寄存器中的内存是不确定的,需要从主存中读取。synchronized主要是解决多个线程访问资源的同步性。
  • volatile作用于变量,synchronized作用于代码块或者方法。
  • volatile仅可以保证数据的可见性,不能保证数据的原子性。synchronized可以保证数据的可见性和原子性。
  • volatile不会造成线程的阻塞,synchronized会造成线程的阻塞。

synchronized和Lock的区别?

  • Lock是显示锁,需要手动开启和关闭。synchronized是隐士锁,可以自动释放锁。
  • Lock是一个接口,是JDK实现的。synchronized是一个关键字,是依赖JVM实现的。
  • Lock是可中断锁,synchronized是不可中断锁,需要线程执行完才能释放锁。
  • 发生异常时,Lock不会主动释放占有的锁,必须通过unlock进行手动释放,因此可能引发死锁。synchronized在发生异常时会自动释放占有的锁,不会出现死锁的情况。
  • Lock可以判断锁的状态,synchronized不可以判断锁的状态。
  • Lock实现锁的类型是可重入锁、公平锁。synchronized实现锁的类型是可重入锁,非公平锁。
  • Lock适用于大量同步代码块的场景,synchronized适用于少量同步代码块的场景。

看到这里了,说明我的文章对你有一定帮助,微信搜索公众号路人zhang,回复面试手册,领取我总结的更多高频面试题PDF版。

本文转载自: 掘金

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

Jetpack Compose 架构如何选? MVP, MV

发表于 2021-06-03

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

本次 I/O 大会上曝出了 Compose 1.0 即将发布的消息,虽然 API 层面已趋于稳定,但真正要在项目中落地还少不了一套合理的应用架构。传统 Android 开发中的 MVP、MVVM 等架构在声明式UI这一新物种中是否还依旧可用呢?

本文以一个简单的业务场景为例,试图找出一种与 Compose 最契合的架构模式

Sample : Wanandroid Search

App基本功能:用户输入关键字,在 wanandroid 网站中搜索出相关内容并展示

Wanandroid Search

功能虽然简单,但是集合了数据请求、UI展示等常见业务场景,可用来做UI层与逻辑层的解耦实验。

前期准备:Model层

其实无论 MVX 中 X 如何变化, Model 都可以用同一套实现。我们先定义一个 DataRepository ,用于从 wanandroid 获取搜索结果。 后文Sample中的 Model 层都基于此 Repo 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码@ViewModelScoped
class DataRepository @Inject constructor(){

private val okhttpClient by lazy {
OkHttpClient.Builder().build()
}

private val apiService by lazy {
Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.client(okhttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(ApiService::class.java)
}


suspend fun getArticlesList(key: String) =
apiService.getArticlesList(key)
}

Compose为什么需要架构?

首先,先看看不借助任何架构的 Compose 代码是怎样的?

不使用架构的情况下,逻辑代码将与UI代码偶合在一起,在Compose中这种弊端显得尤为明显。常规 Android 开发默认引入了 MVC 思想,XML的布局方式使得UI层与逻辑层有了初步的解耦。但是 Compose 中,布局和逻辑同样都使用Kotlin实现,当布局中夹了杂逻辑,界限变得更加模糊。

此外,Compose UI中混入逻辑代码会带来更多的潜在隐患。由于 Composable 会频繁重组,逻辑代码中如果涉及I/O 就必须当做 SideEffect{} 处理、一些不能随重组频繁创建的对象也必须使用 remember{} 保存,当这些逻辑散落在UI中时,无形中增加了开发者的心智负担,很容易发生遗漏。

Sample 的业务场景特别简单,UI中出现少许 remember{} 、LaunchedEffect{} 似乎也没什么不妥,对于一些相对简单的业务场景出现下面这样的代码没有问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码@Composable
fun NoArchitectureResultScreen(
answer: String
) {

val isLoading = remember { mutableStateOf(false) }

val dataRepository = remember { DataRepository() }

var result: List<ArticleBean> by remember { mutableStateOf(emptyList()) }

LaunchedEffect(Unit) {
isLoading.value = true
result = withContext(Dispatchers.IO) { dataRepository.getArticlesList(answer).data.datas }
isLoading.value = false
}

SearchResultScreen(result, isLoading.value , answer)

}

但是,当业务足够复杂时,你会发现这样的代码是难以忍受的。这正如在 React 前端开发中,虽然 Hooks 提供了处理逻辑的能力,但却依然无法取代 Redux。

MVP、MVVM、MVI 是 Android中的一些常见架构模式,它们的目的都是服务于UI层与逻辑层的解耦,只是在解耦方式上有所不同,如何选择取决于使用者的喜好以及项目的特点

“没有最好的架构,只有最合适的架构。”

那么在 Compose 项目中何种架构最合适呢?

MVP

MVP 主要特点是 Presenter 与 View 之间通过接口通信, Presenter 通过调用 View 的方法实现UI的更新。

mvp

这要求 Presenter 需要持有一个 View 层对象的引用,但是 Compose 显然无法获得这种引用,因为用来创建 UI 的 Composable 必须要求返回 Unit,如下:

1
2
3
4
5
6
kotlin复制代码@Composable
fun HomeScreen() {
Column {
Text("Hello World!")
}
}

官方文档中对无返回值的要求也进行了明确约束:

The function doesn’t return anything. Compose functions that emit UI do not need to return anything, because they describe the desired screen state instead of constructing UI widgets.
developer.android.com/jetpack/com…

Compose UI 既然存在于 Android 体系中,必定需要有一个与 Android 世界连接的起点,起点处可能是一个 Activity 或者 Fragment,用他们做UI层的引用句柄不可以吗?

理论上可以,但是当 Activity 接收 Presenter 通知后,仍然无法在内部获取局部引用,只能设法触发整体Recomposition,这完全丧失了 MVP 的优势,即通过获取局部引用进行精准刷新。

通过分析可以得到结论: “MVP 这种依赖接口通信的解耦方式无法在 Compose 项目中使用”

MVVM(without Jetpack)

相对于 MVP 的接口通信 ,MVVM 基于观察者模式进行通信,当 UI 观察到来自 ViewModle 的数据变化时自我更新。 UI层是否能返回引用句柄已不再重要,这与 Compose 的工作方式非常契合。

mvvm

自从 Android 用 ViewModel 命名了某 Jetpack 组件后,在很多人心里,Jetpack 似乎就与 MVVM 画上了等号。这确实客观推动了 MVVM 的普及,但是 Jetpack 的 ViewModel 并非只能用在 MVVM 中(比如如后文介绍的 MVI 也可以使用 ); 反之,没有 Jetpack ,照样可以实现 MVVM。

先来看看不借助 Jetpack 的情况下,MVVM 如何实现?

Activity 中创建 ViewModel

首先 View 层创建 ViewModel 用于订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class MvvmActivity : AppCompatActivity() {

private val mvvmViewModel = MvvmViewModel(DataRepository())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePlaygroundTheme {
MvvmApp(mvvmViewModel) //将vm传给Composable
}
}
}
}

Compose 项目一般使用单 Activity 结构, Activity 作为全局入口非常适合创建全局 ViewModel。 子 Compoable 之间需要基于 ViewModel 通信,所以构建 Composable 时将 ViewModel 作为参数传入。

Sample 中我们在 Activity 中创建的 ViewModel 仅仅是为了传递给 MvvmApp 使用,这种情况下也可以通过传递 Lazy<MvvmViewModel>,将创建延迟到真正需要使用的时候以提高性能。

定义 NavGraph

当涉及到 Compose 页面切换时,navigation-compose 是一个不错选择,Sample中也特意设计了SearchBarScreen 和 SearchResultScreen 的切换场景

1
2
groovy复制代码// build.gradle
implementation "androidx.navigation:navigation-compose:$latest_version"
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
kotlin复制代码
@Composable
fun MvvmApp(
mvvmViewModel: MvvmViewModel
) {
val navController = rememberNavController()

LaunchedEffect(Unit) {
mvvmViewModel.navigateToResults
.collect {
navController.navigate("result") //订阅VM路由事件通知,处理路由跳转
}
}

NavHost(navController, startDestination = "searchBar") {
composable("searchBar") {
MvvmSearchBarScreen(
mvvmViewModel,
)
}
composable("result") {
MvvmSearchResultScreen(
mvvmViewModel,
)
}
}
}
  • 在 root-level 的 MvvmApp 中定义 NavGraph, composable("$dest_id"){} 中构造路由节点的各个子 Screen,构造时传入 ViewModel 用于 Screen 之间的通信
  • 每个 Composable 都有一个 CoroutineScope 与其 Lifecycle 绑定,LaunchedEffect{} 可以在这个 Scope 中启动协程处理副作用。 代码中使用了一个只执行一次的 Effect 订阅 ViewModel 的路由事件通知
  • 当然我们可以将 navConroller 也传给 MvvmSearchBarScreen ,在其内部直接发起路由跳转。但在较复杂的项目中,跳转逻辑与页面定义应该尽量保持解耦,这更利于页面的复用和测试。
  • 我们也可以在 Composeable 中直接 mutableStateOf() 创建 state 来处理路由跳转,但是既然选择使用 ViewModel 了,那就应该尽可能将所有 state 集中到 ViewModle 管理。

注意: 上面例子中的处理路由跳转的 navigateToResults 是一个“事件”而非“状态”,关于这部分区别,在后文在详细阐述

定义子 Screen

接下来看一下两个 Screen 的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码@Composable
fun MvvmSearchBarScreen(
mvvmViewModel: MvvmViewModel,
) {

SearchBarScreen {
mvvmViewModel.searchKeyword(it)
}

}

@Composable
fun MvvmSearchResultScreen(
mvvmViewModel: MvvmViewModel
) {

val result by mvvmViewModel.result.collectAsState()
val isLoading by mvvmViewModel.isLoading.collectAsState()

SearchResultScreen(result, isLoading, mvvmViewModel.key.value)

}

大量逻辑都抽象到 ViewModel 中,所以 Screen 非常简洁

  • SearchBarScreen 接受用户输入,将搜索关键词发送给 ViewModel
  • MvvmSearchResultScreen 作为结果页显示 ViewModel 发送的数据,包括 Loading 状态和搜索结果等。
  • collectAsState 用来将 Flow 转化为 Compose 的 state,每当 Flow 接收到新数据时会触发 Composable 重组。 Compose 同时支持 LiveData、RxJava 等其他响应式库的collectAsState

UI层的更多内容可以查阅 SearchBarScreen 和 SearchResultScreen 的源码。经过逻辑抽离后,这两个 Composable 只剩余布局相关的代码,可以在任何一种 MVX 中实现复用。

ViewModel 实现

最后看一下 ViewModel 的实现

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
kotlin复制代码class MvvmViewModel(
private val searchService: DataRepository,
) {

private val coroutineScope = MainScope()
private val _isLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
private val _result: MutableStateFlow<List<ArticleBean>> = MutableStateFlow(emptyList())
val result = _result.asStateFlow()
private val _key = MutableStateFlow("")
val key = _key.asStateFlow()

//使用Channel定义事件
private val _navigateToResults = Channel<Boolean>(Channel.BUFFERED)
val navigateToResults = _navigateToResults.receiveAsFlow()

fun searchKeyword(input: String) {
coroutineScope.launch {
_isLoading.value = true
_navigateToResults.send(true)
_key.value = input
val result = withContext(Dispatchers.IO) { searchService.getArticlesList(input) }
_result.emit(result.data.datas)
_isLoading.value = false
}
}
}
  • 接收到用户输入后,通过 DataRepository 发起搜索请求
  • 搜索过程中依次更新 loading(loading显示状态)、navigateToResult(页面跳转事件)、 key(搜索关键词)、result(搜索结果)等内容,不断驱动UI刷新

所有状态集中在 ViewModel 管理,甚至页面跳转、Toast弹出等事件也由 ViewModel 负责通知,这对单元测试非常友好,在单测中无需再 mock 各种UI相关的上下文。

Jetpack MVVM

Jeptack 的意义在于降低 MVVM 在 Android平台的落地成本。

引入 Jetpack 后的代码变化不大,主要变动在于 ViewModel 的创建。

Jetpack 提供了多个组件,降低了 ViewModel 的使用成本:

  • 通过 hilt 的 DI 降低 ViewModel 构造成本,无需手动传入 DataRepository 等依赖
  • 任意 Composable 都可以从最近的 Scope 中获取 ViewModel,无需层层传参。
1
2
3
4
5
6
kotlin复制代码@HiltViewModel
class JetpackMvvmViewModel @Inject constructor(
private val searchService: DataRepository // DataRepository 依靠DI注入
) : ViewModel() {
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码@Composable
fun JetpackMvvmApp() {
val navController = rememberNavController()

NavHost(navController, startDestination = "searchBar", route = "root") {
composable("searchBar") {
JetpackMvvmSearchBarScreen(
viewModel(navController, "root") //viewModel 可以在需要时再获取, 无需实现创建好并通过参数传进来
)
}
composable("result") {

JetpackMvvmSearchResultScreen(
viewModel(navController, "root") //可以获取跟同一个ViewModel实例
)
}
}

}
1
2
3
4
5
6
7
8
9
kotlin复制代码@Composable
inline fun <reified VM : ViewModel> viewModel(
navController: NavController,
graphId: String = ""
): VM =
//在 NavGraph 全局范围使用 Hilt 创建 ViewModel
hiltNavGraphViewModel(
backStackEntry = navController.getBackStackEntry(graphId)
)

Jetpack 甚至提供了 hilt-navigation-compose 库,可以在 Composable 中获取 NavGraph Scope 或 Destination Scope 的 ViewModel,并自动依赖 Hilt 构建。Destination Scope 的 ViewModel 会跟随 BackStack 的弹出自动 Clear ,避免泄露。

1
2
groovy复制代码// build.gradle
implementation androidx.hilt:hilt-navigation-compose:$latest_versioin

“未来 Jetpack 各组件之间协同效应会变得越来越强。”

MVI

MVI 与 MVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,可以看做是 MVVM + Redux 的结合。

MVI 的 I 指 Intent,这里不是启动 Activity 那个 Intent,而是一种对用户操作的封装形式,为避免混淆,也可唤做 Action 等其他称呼。 用户操作以 Action 的形式送给 Model层 进行处理。代码中,我们可以用 Jetpack 的 ViewModel 负责 Intent 的接受和处理,因为 ViewModel 可以在 Composable 中方便获取。

mvi

在 SearchBarScreen 用户输入关键词后通过 Action 通知 ViewModel 进行搜索

1
2
3
4
5
6
7
8
9
kotlin复制代码@Composable
fun MviSearchBarScreen(
mviViewModel: MviViewModel,
onConfirm: () -> Unit
) {
SearchBarScreen {
mviViewModel.onAction(MviViewModel.UiAction.SearchInput(it))
}
}

通过 Action 通信,有利于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码@Composable
fun MviSearchResultScreen(
mviViewModel: MviViewModel
) {
val viewState by mviViewModel.viewState.collectAsState()

SearchResultScreen(
viewState.result, viewState.isLoading, viewState.key
)

}

MVVM 的 ViewModle 中分散定义了多个 State ,MVI 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。

相对于 MVVM,ViewModel 也有一些变化

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
kotlin复制代码class MviViewModel(
private val searchService: DataRepository,
) {

private val coroutineScope = MainScope()

private val _viewState: MutableStateFlow<ViewState> = MutableStateFlow(ViewState())
val viewState = _viewState.asStateFlow()

private val _navigateToResults = Channel<OneShotEvent>(Channel.BUFFERED)
val navigateToResults = _navigateToResults.receiveAsFlow()

fun onAction(uiAction: UiAction) {
when (uiAction) {
is UiAction.SearchInput -> {
coroutineScope.launch {
_viewState.value = _viewState.value.copy(isLoading = true)
val result =
withContext(Dispatchers.IO) { searchService.getArticlesList(uiAction.input) }
_viewState.value =
_viewState.value.copy(result = result.data.datas, key = uiAction.input)
_navigateToResults.send(OneShotEvent.NavigateToResults)
_viewState.value = _viewState.value.copy(isLoading = false)
}
}
}
}

data class ViewState(
val isLoading: Boolean = false,
val result: List<ArticleBean> = emptyList(),
val key: String = ""
)

sealed class OneShotEvent {
object NavigateToResults : OneShotEvent()
}

sealed class UiAction {
class SearchInput(val input: String) : UiAction()
}
}
  • 页面所有的状态都定义在 ViewState 这个 data class 中,状态的修改只能在 onAction 中进行, 其余场所都是 immutable 的, 保证了数据流只能单向修改。 反观 MVVM ,MutableStateFlow 对外暴露时转成 immutable 才能保证这种安全性,需要增加不少模板代码且仍然容易遗漏。
  • 事件则统一定义在 OneShotEvent中。 Event 不同于 State,同一类型的事件允许响应多次,因此定义事件使用 Channel 而不是 StateFlow。

Compose 鼓励多使用 State 少使用 Event, Event 只适合用在弹 Toast 等少数场景中

通过浏览 ViewModel 的 ViewState 和 Aciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。

页面路由

Sample 中之所以使用事件而非状态来处理路由跳转,一个主要原因是由于使用了 Navigation。Navigation 有自己的 backstack 管理,当点击 back 键时会自动帮助我们返回前一页面。倘若我们使用状态来描述当前页面,当点击 back时,没有机会更新状态,这将造成 ViewState 与 UI 的不一致。

关于路由方案的建议:简单项目使用事件控制页面跳转没有问题,但是对于复杂项目,推荐使用状态进行页面管理,有利于逻辑层时刻感知到当前的UI状态。

我们可以将 NavController 的 backstack 状态 与 ViewModel 的状态建立同步:

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
kotlin复制代码
class MvvmViewModel(
private val searchService: DataRepository,
) {

...
//使用 StateFlow 描述页面
private val _destination = MutableStateFlow(DestSearchBar)
val destination = _destination.asStateFlow()

fun searchKeyword(input: String) {
coroutineScope.launch {
...
_destination.value = DestSearchResult
...
}
}

fun bindNavStack(navController: NavController) {
//navigation 的状态时刻同步到 viewModel
navController.addOnDestinationChangedListener { _, _, arguments ->
run {
_destination.value = requireNotNull(arguments?.getString(KEY_ROUTE))
}
}
}
}

如上,当 navigation 状态变化时,会及时同步到 ViewModel ,这样就可以使用 StateFlow 而非 Channel 来描述页面状态了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码@Composable
fun MvvmApp(
mvvmViewModel: MvvmViewModel
) {
val navController = rememberNavController()

LaunchedEffect(Unit) {
with(mvvmViewModel) {
bindNavStack(navController) //建立同步
destination
.collect {
navController.navigate(it)
}
}
}
}

在入口处,为 NavController 和 ViewModel 建立同步绑定即可。

Clean Architecture

更大型的项目中,会引入 Clean Architecture ,通过 Use Case 将 ViewModel 内的逻辑进一步分解。 Compose 只是个 UI 框架,对于 ViewModle 以下的逻辑层的治理方式与传统的 Andorid 开发没有区别。所以 Clean Architecture 这样的复杂架构仍然可以在 Compose 项目中使用

总结

比较了这么多种架构,那种与 Compose 最契合呢?

Compose 的声明式UI思想来自 React,所以同样来自 Redux 思想的 MVI 应该是 Compose 的最佳伴侣。当然 MVI 只是在 MVVM 的基础上做了一些改良,如果你已经有了一个 MVVM 的项目,只是想将 UI 部分改造成 Compose ,那么没必要为了改造成 MVI 而进行重构,MVVM 也可以很好地配合 Compose 使用的。 但是如果你想将一个 MVP 项目改造成 Compose 可能成本就有点大了。

关于 Jetpack,如果你的项目只用于 Android,那么 Jetpack 无疑是一个好工具。但是 Compose 未来的应用场景将会很广泛,如果你有预期未来会配合 KMP 开发跨平台应用,那么就需要学会不依赖 Jetpack 的开发方式,这也是本文为什么要介绍非 Jetpack 下的 MVVM 的一个初衷。

Sample代码

github.com/vitaviva/Je…

本文转载自: 掘金

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

SpringBoot整合Redis--让你的项目`快`起来吧

发表于 2021-06-03

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

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接


相关文章

Redis实战汇总:Redis实战


前言

SpringBoot应该不用过多介绍了吧!是Spring当前最火的一个框架,既然学习了Redis,我们肯定是要在实际项目中使用,那么肯定首选整合SpringBoot啦!

简单介绍下SpringBoot对Jedis的支持吧,在1.×版本的时候,SpringBoot的底层还是使用Jedis来连接Redis的,但是在2.×版本后,就换成了Lettuce。两者的区别如下:
Jedis: 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接池! 更像 BIO 模式!
Lettuce: 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像 NIO 模式!

  1. 添加POM依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码	<dependencies>
<!--集成redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<!--序列化-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
<scope>compile</scope>
</dependency>
<!--lombok,自动生成set、get等方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>compile</scope>
</dependency>
  1. 配置连接的application.yml文件:
1
2
3
4
5
6
7
bash复制代码server:
port: 10001

spring:
redis:
host: 127.0.0.1
port: 6379
  1. 测试连接:我们写个测试方法来进行最基本的连接测试!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
public class testRedis {

@Autowired(required = false)
private RedisTemplate redisTemplate;

@Test
void getName(){
redisTemplate.opsForValue().set("name","dadadingdada!");
System.out.println(redisTemplate.opsForValue().get("name"));
}
}

运行效果如下!证明Redis连接成功!并且加数据获取数据也成功了!
在这里插入图片描述

  1. 其他方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码// redisTemplate  #操作不同的数据类型,api和我们的指令是一样的 
// opsForValue #操作字符串 类似String
// opsForList #操作List 类似List
// opsForSet #操作set
// opsForHash #操作hash
// opsForZSet #操作zset
// opsForGeo #操作geo
// opsForHyperLogLog #操作HyperLogLog
// 除了进本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的 CRUD
// 获取redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb();
// connection.flushAll();

证明如果是用基本RedisTemplate类来操作Redis的话,是基本上可以达到所有的效果的,因为具体方法和命令大体一致!

  1. 对象的保存和读取
    新增一个User类:
1
2
3
4
5
6
7
8
java复制代码import lombok.Data;

@Data
public class User {
private String name;
private Integer age;
private Integer high;
}

测试代码如下:

1
2
3
4
5
6
7
8
9
java复制代码@Test
void setObject(){
User user = new User();
user.setName("dingdada");
user.setAge(23);
user.setHigh(172);
redisTemplate.opsForValue().set("user",user);
System.out.println(redisTemplate.opsForValue().get("user"));
}

报错如下:
在这里插入图片描述
结论:所以在操作Redis中,关于对象的保存我们得序列化才可以正常操作!
6. 自定义封装RedisTemplate类
上面说了大体上可以实现,但是为了在工作中更容易操作Redis,我们一般重新封装RedisTemplate类,如下所示:

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
java复制代码import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
// 这是我给大家写好的一个固定模板,大家在企业中,拿去就可以直接使用!
// 自己定义了一个RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 我们为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

序列化完成后我们再试试对象的添加获取:
在这里插入图片描述
NICE!一切正常!

  1. 封装RedisUtils类:
    在实际工作中,我们不可能用RedisTemplate 来操作Redis的,因为实在太繁琐,所以我们一般自定义一个RedisUtils工具类来操作Redis!
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
java复制代码import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

@Resource
private RedisTemplate<String, Object> redisTemplate;


public Set<String> keys(String keys){
try {
return redisTemplate.keys(keys);
}catch (Exception e){
e.printStackTrace();
return null;
}
}

/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入, 不存在放入,存在返回
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean setnx(String key, Object value) {
try {
redisTemplate.opsForValue().setIfAbsent(key,value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 普通缓存放入并设置时间,不存在放入,存在返回
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean setnx(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0){
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}

基本上常用的Redis操作都写在这里了,我们在工作中需要用什么,直接通过RedisUtils来使用即可!

  1. 使用RedisUtils
    ①首先将springboot项目启动起来:
    在这里插入图片描述
    ②添加Contoller在web上测试添加和获取!
    在这里插入图片描述

③测试工具类的基本使用:
在谷歌浏览器输入:返回成功
在这里插入图片描述
测试获取:获取成功!
在这里插入图片描述

9.总结:其实这篇讲了这么多,大家可以发现,SpringBoot真的是极度方便,整合Redis之后我们只需要简单的操作即可完美使用Redis!
但是,前面的内容还都是Redis相关的基础,接下来我将继续整理关于Redis的进阶知识!


路漫漫其修远兮,吾必将上下求索~
到此关于SpringBoot如何整合Redis的讲解就算告一段落了,如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~

本文转载自: 掘金

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

SpringCloud Alibaba实战(2:电商系统业务

发表于 2021-06-03

选用了很常见的电商业务来进行SpringCloud Alibaba的实战。

当然,因为仅仅是为了学习SpringCloud Alibaba,所以对业务进行了大幅度简化,这里只取一个精简版的用户下单业务。

1、电商业务流程

电商系统下单业务流程图:

电商系统下单业务

这个流程同样进行了简化,一般浏览完商品可能不会直接下单,而是先加入购物车,这里我们略去了这一环节。

接下来,我们要对业务进行细化,可以通过时序图来对业务流程进行细化。

用户下单示意图

2、扣库存时机

注意看,在我们的泳道图中,扣库存是在用户下订单之后。

在电商系统里,扣库存一般主要有两种方式:

  • 下单扣库存:下单扣库存是用户体验比较好的方式,避免用户支付的时候发现库存不足。缺点是不合适商品库存有限的情况,因为未付款的订单会影响到其他人购买这款商品。
  • 支付扣库存:支付扣库存对用户体验很不好,因为用户可能在支付的时候发现库存不足。但是比较适合秒杀一类的业务,避免未支付的订单占用库存。

但是下单减库存应该设置一个超时的时间,如果在一定时间内没有完成支付,那么就应该及时释放库存。

zai

3、电商业务流程模块划分

通过上面的时序图,我们对电商下单的业务已经有了一个比较清楚的认识,接下来,对具体的业务模块进行划分:

  • 1、用户模块:需要有一个用户服务,用于用户基本信息的管理,包括用户名、等级等等。
  • 2、商品模块:用户浏览商品,需要有一个 商品模块来支撑,给用户展示商品的介绍、价格等等这些信息。
  • 3、订单模块:用户下单需要订单模块来创建订单。
  • 4、支付模块:订单创建完成,需要用户付款,这里需要有一个 支付模块 来实现支付功能,用户成功完成支付之后,需要把订单的状态变更为 「已支付」。
  • 5、库存模块:支付完成,就需要运营人员发货,这个步骤,需要扣减对应商品的库存数量,所以要有库存模块,发货完成后,还需要把订单状态变更为「已发货」。

当然,真实的业务比这要复杂很多,我们只考虑一个简单的用户下单业务,所以主要业务模块就是这些。

用户下单业务模块划分

我们采用纵向划分的服务划分方式,以义务为维度,对服务进行划分。

“简单的事情重复做,重复的事情认真做,认真的事情有创造性地做!”——

我是三分恶,可以叫我老三/三分/三哥/三子,一个能文能武的全栈开发,咱们下期见!


参考:

【1】:小专栏 SpringCloudAlibaba微服务实战
【2】:电商系统是如何设计的?

【4】:详解:电商前端库存逻辑的设计

本文转载自: 掘金

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

SpringBoot 整合 WebService 加入依赖

发表于 2021-06-03

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

日积月累,水滴石穿 😄

在工作当中经常需要与第三方对接,某些第三方提供的接口是 WebService 类型的,所以需要集成 WebService
由于 SpringBoot提供了 WebService的 starter 组件,所以集成 WebService相当的简单

加入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.3.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

服务端

创建WebService接口

1
2
3
4
5
6
7
8
9
java复制代码package com.gongj.webservice_server.service;

import com.gongj.webservice_server.DTO.UserDTO;
import javax.jws.WebService;

public interface IUserServer {

UserDTO getUser(Long str);
}

创建实体类

1
2
3
4
5
6
7
8
java复制代码@Data
public class UserDTO {

private Long id;
private String name;
private Integer age;
private String address;
}

创建WebService接口的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.gongj.webservice_server.service.impl;

import com.gongj.webservice_server.DTO.UserDTO;
import com.gongj.webservice_server.service.IUserServer;
import org.springframework.stereotype.Service;

import javax.jws.WebService;

@Service
@WebService
public class UserServerImpl implements IUserServer {
@Override
public UserDTO getUser(Long id) {
UserDTO user = new UserDTO();
user.setId(id);
user.setAddress("上海市浦东新区");
user.setAge(25);
user.setName("gongj");
return user;
}
}

这里用到了一个注解@WebService,我这就只在实现类上使用了。这里介绍一下,先来看下它的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface WebService {
String name() default "";

String targetNamespace() default "";

String serviceName() default "";

String portName() default "";

String wsdlLocation() default "";

String endpointInterface() default "";
}
  • name:对应 wsdl:portType 标签,默认值为 Java 类或接口的名称
  • targetNamespace:命名空间,一般写为接口的包名倒序,默认值也是接口的包名倒序。对应 wsdl:definitions:targetNamespace 标签,
  • serviceName:WebService的服务名称,对应wsdl:service,默认值为WebService接口实现类的名称+ “Service”,示例:UserServerImplService
  • portName:对应 wsdl:port标签,默认值为:WebService接口实现类的名称 + “Port”,示例:UserServerImplPort
  • wsdlLocation:指定用于定义 WebService 的 WSDL 文档的地址
  • endpointInterface:WebService 接口全路径

创建 WebService 配置类

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
java复制代码package com.gongj.webservice_server.config;

import com.gongj.webservice_server.service.IUserServer;
import org.apache.cxf.Bus;
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.xml.ws.Endpoint;

@Configuration
public class CxfConfig {

@Autowired
private IUserServer userServer;
/**
* 注入Servlet 注意beanName不能为dispatcherServlet
* @return
*/
@Bean
public ServletRegistrationBean cxfServlet() {
return new ServletRegistrationBean(new CXFServlet(),"/webservice/*");
}

@Bean(name = Bus.DEFAULT_BUS_ID)
public SpringBus springBus() {
return new SpringBus();
}


@Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(springBus(), userServer);
endpoint.publish("/api");
return endpoint;
}
}

启动服务,进行访问:http://localhost:8080/webservice

image.png
点击链接跳转,我们会看到一个页面,这是 wsdl 服务描述文档。对于这个文档也需要简单的了解一下,也许某次对接第三方系统直接给你一个 wsdl 文档让你自己看去,注意:wsdl 文档是从下往上看的。

image.png

wsdl 文档

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
xml复制代码<?xml version='1.0' encoding='UTF-8'?>
<wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:tns="http://impl.service.webservice_server.gongj.com/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="UserServerImplService"
targetNamespace="http://impl.service.webservice_server.gongj.com/">


<!-- web service 使用的数据类型 -->
<wsdl:types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://impl.service.webservice_server.gongj.com/" elementFormDefault="unqualified" targetNamespace="http://impl.service.webservice_server.gongj.com/" version="1.0">

<xs:element name="getUser" type="tns:getUser"/>

<xs:element name="getUserResponse" type="tns:getUserResponse"/>

<!-- getUser 方法的请求参数类型 -->
<xs:complexType name="getUser">
<xs:sequence>
<xs:element minOccurs="0" name="arg0" type="xs:long"/>
</xs:sequence>
</xs:complexType>

<!-- getUser 方法的响应参数类型 -->
<xs:complexType name="getUserResponse">
<xs:sequence>
<xs:element minOccurs="0" name="return" type="tns:userDTO"/>
</xs:sequence>
</xs:complexType>

<!-- getUser 方法的响应参数的具体类型 -->
<xs:complexType name="userDTO">
<xs:sequence>
<xs:element minOccurs="0" name="address" type="xs:string"/>
<xs:element minOccurs="0" name="age" type="xs:int"/>
<xs:element minOccurs="0" name="id" type="xs:long"/>
<xs:element minOccurs="0" name="name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
</wsdl:types>

<!--
message:用来定义soap消息结构
part:引用上面 types 中的约束格式
-->
<wsdl:message name="getUser">
<wsdl:part element="tns:getUser" name="parameters">
</wsdl:part>
</wsdl:message>
<wsdl:message name="getUserResponse">
<wsdl:part element="tns:getUserResponse" name="parameters">
</wsdl:part>
</wsdl:message>

<!--
portType:用来指定服务器端的接口
operation:接口中定义的方法
input:方法getUser的输入
output:方法getUser的输出
输入输出引用的是上面message的定义
-->
<wsdl:portType name="UserServerImpl">
<wsdl:operation name="getUser">
<wsdl:input message="tns:getUser" name="getUser">
</wsdl:input>
<wsdl:output message="tns:getUserResponse" name="getUserResponse">
</wsdl:output>
</wsdl:operation>
</wsdl:portType>

<!--
type属性:引用<portType>的定义
<soap:binding style="document">:表示传输的一个document (xml)
<input><output>方法的输入、输出
<soap:body use="literal" />:表示body传输采用文本即xml格式的文本
-->
<wsdl:binding name="UserServerImplServiceSoapBinding" type="tns:UserServerImpl">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getUser">
<soap:operation soapAction="" style="document"/>
<wsdl:input name="getUser">
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output name="getUserResponse">
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>

<!--
name:用于指定客户端的容器类/工厂类
binding:引用上面的 binding 标签
port name:容器通过这个方法获得实现类,自动生成会出现这个类,可以不用管
address:客户端真正用于请求的地址
-->
<wsdl:service name="UserServerImplService">
<wsdl:port binding="tns:UserServerImplServiceSoapBinding" name="UserServerImplPort">
<soap:address location="http://localhost:8080/webservice/api"/>
</wsdl:port>
</wsdl:service>

</wsdl:definitions>

客户端

加入依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.3.4</version>
</dependency>

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static void main(String[] args) {
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
Client client = dcf.createClient("http://localhost:8080/webservice/api?wsdl");
ObjectMapper mapper = new ObjectMapper();
try {
// invoke("方法名",参数1,参数2,参数3....);
Object[] objects = client.invoke("getUser", 99L);
System.out.println(mapper.writeValueAsString(objects[0]));
} catch (java.lang.Exception e) {
e.printStackTrace();
}
}

输出结果:
{"address":"上海市浦东新区","age":25,"id":99,"name":"gongj"}
  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。

本文转载自: 掘金

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

如何让Android 支持HEIF 图片解码和加载(免费的方

发表于 2021-06-03

字节跳动火山引擎ImageX提供了一种能力,可以支持客户端android 直接解码HEIF 和HEIC图片,经过测试发现,可以免费使用;

一、阅前准备

  • HEIF图片格式是什么?

高效率图像格式(High Efficiency Image Format ,HEIF)最早被苹果公司的 iPhone 所使用,并且也将用于 Google 的 Android P 手机系统。微软也于最新放出的 Windows 10 Build 17123 预览版开始,新增了对 HEIF 图像格式的系统原生支持,所以系统极客将在本文中为大家简介 HEIF 这一新兴的高效率图像格式。

  • HEIF优于JPEG图像格式

高效率图像格式在各方面均优于 JPEG,通过使用更现代的压缩算法,它可以将相同数量的数据大小压缩到 JPEG 图像文件的 50% 左右。随着手机 Camera 的不断升级,照片的细节也日益增加。通过将照片存储为 HEIF 格式而不非 JPEG,可以让文件大小减半,几乎可以在同一部手机上存储以前 2 倍的照片数量。如果一些云服务也支持 HEIF 文件,则上传到在线服务的速度也会更快,并且使用更少的存储空间。在 iPhone 上,这意味着您的照片应该会以以前两倍的速度上传到 iCloud 照片库。

JPEG 标准可以追溯到 1992 年,JPEG 标准的最新版本也于 1994 年完成,JPEG 长期以来为我们提供了很好的服务,但现代(新)标准超越它并不是很奇怪。

  • HEIC唯一缺点:兼容性

目前使用 HEIF 或 HEIC 照片唯一的缺点就是兼容性问题。现在的软件只要能够查看图片,那它肯定就可以读取 JPEG 图像,但如果你拍摄了以 HEIF 或 HEIC 扩展名结尾的图片,并不是在所有地方和软件中都可以正确识别。

这也是当我们将照片附加到电子邮件或在不支持 HEIF 文件的服务中进行共享时, iPhone 和 iPad 会自动将其转换为 JPEG 图像的原因。在使用 iTunes 将 HEIF 照片导入 Windows PC 时,也会自动将它们转换为 JPEG 格式。

虽然 Mac 从 macOS High Sierra 开始支持 .HEIF 和 .HEIC 文件,但 Windows 10 从 Windows 10 Build 17123 预览版才开始提供 HEIF 图像内置支持,所以对于老旧 Windows、macOS 和旧版 iOS 与 Android 用户需要使用第三方图像查看器或转换软件才能查看 .HEIF 或 .HEIC 文件。

那我们如何让HEIF 支持全端Android 机型呢? 这里提供了一种软解码实现方案,具体接入如下:

二、遇到了什么问题

  • HEIF 图片在iOS 11以上开始支持,但是在Android 系统支持 一直比较慢,而且有很多系统性的bug 会导致解码失败;所以我们干脆实现一种软解的方案;
  • HEIF 使用自研的解码能力,发现使用HEIF 图片加载整体体积降低 50%+,用户加载更快!
  • 针对线上图片性能、图片进行可用性、网络耗时的监控,全面感知客户端的图片加载问题;

三、开发环境

推荐开发者使用 Android Studio 作为自己的开发工具,本开发文档也是基于 Android Studio开发环境下进行编写的。

四、集成方式

详细阅读:www.volcengine.com/docs/508/65…

  1. 项目 build.gradle 下加上
1
2
3
rust复制代码maven {
url 'https://dl.bintray.com/ttgamesdk/public'
}
  1. app module build.gradle下加上
1
2
3
4
5
6
7
arduino复制代码implementation 'com.bytedance.fresco:fresco:1.0.4'
implementation "com.bytedance.fresco:animated-gif:1.0.4", //gif
implementation "com.bytedance.fresco:animated-webp:1.0.4", //webp animated
implementation "com.bytedance.fresco:webpsupport:1.0.4", //低版本webp支持
implementation "com.bytedance.fresco:drawee:1.0.4", //fresco组件
implementation "com.bytedance.fresco:statistics:1.0.4", //监控+网络组件
implementation "com.bytedance.fresco:heif:1.0.4"

五、接入说明

1. 初始化

SDK在集成之前需要将此AppID传入参数里(需要注意的是AppID在Android端SDK中也简写为“aid”),部分参数含义详解如下:

  • AppID(aid):SDK用于打点监控上报的最小单元,通过此将数据进行隔离上报,同时通过AppID可以拉取对应的云控配置比如客户端采样率、网络优化参数等。
  • deviceId:设备的唯一编号,用于统计区分使用。
  • versionName,versionCode:主要用于数据统计与配置拉取。
  • channel:渠道标识,用于区分统计,比如根据不同频道可以传入huawei、oppo等不同渠道便于自动以统计。
  • appName:App的名称,用于统计使用。
  • isOversea:主要根据App是否发布海外决定上报的日志的物理位置,满足GDPR合规性要求,如实填写,比如App为南美使用,则传入 true即可,采样后的日志自动上报到海外地区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码String aid =  "xxx";                // App ID
String deviceId = "xxx" ; // 设备 ID
String versionName = "0.0.1" ; // App 版本号
String versionCode = "1" ; // App 版本code
String channel = "debug" ; // 渠道
String appName = "Sample" ; // App 名称
boolean isOversea = false; // App是否发布在海外
_// 统计功能_
Set<RequestListener> listeners = new HashSet<>();
listeners.add( new FrescoTraceListener( this , aid, deviceId, versionName, channel, isOversea));


_// HEIF功能配置_
PoolFactory factory = new PoolFactory(PoolConfig. _newBuilder_ ().build());
ImagePipelineConfig.Builder builder = ImagePipelineConfig. _newBuilder_ ( this )
.setNetworkFetcher( new FrescoTTNetFetcher( this , aid, deviceId,versionCode, versionName,
channel, appName))
.setRequestListeners(listeners)
.setImageDecoderConfig(ImageDecoderConfig. _newBuilder_ ().addDecodingCapability(
HeifDecoder. _HEIF_FORMAT_ ,
new HeifDecoder.HeifFormatChecker(), new HeifDecoder.HeifFormatDecoder(factory.getPooledByteBufferFactory())).build());


Fresco. _initialize_ ( this , builder.build());

注 :FrescoTraceListener构造参数均不能为null

1
2
3
4
5
6
less复制代码FrescoTraceListener(@NonNull Context context,
@NonNull String aid,
@NonNull String deviceId,
@NonNull String appVersion,
@NonNull String channel,
boolean isOversea)

注 :FrescoTTNetFetcher构造参数均不能为null

1
2
3
4
5
6
7
less复制代码public  FrescoTTNetFetcher(@NonNull Application context, 
@NonNull String appId,
@NonNull String deviceId,
@NonNull String versionCode,
@NonNull String versionName,
@NonNull String channel,
@NonNull String appName)

2. 使用方式

使用方式和正常的Fresco一样,Fresco的 api 并没有修改,参考:www.fresco-cn.org/

3. 单独使用监控功能的方式

如果不想使用改造后的Fresco,使用facebook源的Fresco,可以只使用提供的统计功能。

1
arduino复制代码implementation "com.bytedance.fresco:statistics:1.0.4"
1
2
3
4
5
6
csharp复制代码Set<RequestListener> listeners =   new    HashSet<>();
listeners.add( new FrescoTraceListener(context, "xxx" , "xxxx" , "0.0.1" , "debug" , false ));
ImagePipelineConfig.Builder builder = ImagePipelineConfig. _newBuilder_ ( this )
.setNetworkFetcher( new TTFrescoOkHttpFetcher())
.setRequestListeners(listeners)
Fresco. _initialize_ ( this , builder.build());

4. Feature使用

Android 9.0 libwebp解码

Android 9.0版本上,系统原生的Webp解码方式有bug,这里提供使用libwebp解码的方式。

1
2
3
ini复制代码ImagePipelineConfig  . Builder  builder =   ImagePipelineConfig  .newBuilder(  this  );
// 对9.0版本打开libwebp解码
builder.experiment().setPieDecoderEnabled( true );

性能差异:在honor magic2上对同一图片进行benchmark测试,Android原生解码:15.9ms,libwebp解码:16.4ms,差距不大。

低内存策略

接入方式:
Fresco初始化之前配置以下代码

1
ini复制代码    ImageDecodeBitmapConfigStrategy.setStrategy(ImageDecodeBitmapConfigStrategy. _MEMORY_AT_LEAST_ );

OOM兜底

接入方式:

1
2
ini复制代码ImagePipelineConfig  . Builder  builder =   ImagePipelineConfig  .newBuilder(  this  )
builder.experiment().setOomOptEnabled( true );

动图渐进式

接入方式:

  1. 全局开启:
1
scss复制代码ImagePipelineConfig  .getDefaultImageRequestConfig().setProgressiveRenderingEnabled(  true  );
  1. 单个请求开启:
1
2
3
4
5
6
7
8
9
scss复制代码ImageRequestBuilder  builder =   ImageRequestBuilder  
.newBuilderWithSource(uri)
.setProgressiveRenderingEnabled( true );
DraweeController controller = Fresco .newDraweeControllerBuilder()
.setAutoPlayAnimations( true )
.setImageRequest(builder.build())
.setOldController(getController())
.build();
setController(controller);

智能裁剪

接入方式:

1
2
3
4
5
6
7
8
9
10
scss复制代码ImageRequestBuilder builder = ImageRequestBuilder
. _newBuilderWithSource_ (uri)
.setImageDecodeOptions( new ImageDecodeOptionsBuilder()
.setUseSmartCrop( true )
.build());
DraweeController controller = Fresco. _newDraweeControllerBuilder_ ()
.setImageRequest(builder.build())
.setOldController(getController())
.build();
setController(controller);

本文转载自: 掘金

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

一次接口时延优化与其中的思考 1 背景 2 寻找接口瓶颈

发表于 2021-06-03

好玩的点

  • 3.3 explain
  • 3.5 对象大小
  • 3.6.1 使用具体字段+覆盖索引
  • 4 额外优化-TLAB
  1. 背景

用户查询三个月内会议记录时,返回结果的平均时延高达三四秒。

1.1 优化目标

降低接口时延,响应时间要在200ms以内

优化有三个维度:分别是吞吐量、时延、系统容量。

  • 吞吐量:指的是单位时间内系统能完成多少操作
  • 时延:指的是操作的响应时间,比如说搜索商品的结果必须在200ms内展示给用户
  • 系统容量:指的是在吞吐量和时延达标的情况下,对硬件环境的额外约束

1.2 涉及优化点

网络IO、SQL优化、对象大小、TLAB

1.3 涉及分析工具/命令

arthas、top、jstack、explain

1.4 调优步骤

时延优化.png

我在分析的时候,喜欢先从系统层面去排查分析,比如从CPU、内存、网络、磁盘这几个维度上寻找,从而定位有问题的代码,自上而下。

1.5 压测数据准备

模拟线上用户最大的数据记录,三个月内一共有200条会议记录,每条会议记录平均有十个参会人

数据关系如下

image.png

  1. 寻找接口瓶颈(循环HTTP请求)

2.1 top

压测时用top命令查看CPU的使用率

image.png

发现四核的机子,我们的应用占的CPU只有120%,不太合理,这时候可以用jstack命令看看Java进程里面的线程都在干嘛

2.2 jstack

我们可以用 top -Hp PID来显示进程内所有线程的情况,再把线程对应的PID转成十六进制,再用jstack命令查看该线程的工作情况,但是我觉得这样要一个一个地会比较麻烦

我比较喜欢直接用 jstack -l Java进程的PID > stack.txt,然后把文件拉下来分析。

只jstack一次可能是不准确的,jstack是打印线程快照,那么有可能在某一时刻打印出来的快照是正常的,所以应该多jstack几次来分析

结果发现,有大量线程被阻塞在了java.net.SocketInputStream.socketRead0(Native Method)这上面,根据堆栈信息发现,是代码里面一个循环体里面进行了第三方的接口调用导致的

1
2
3
4
java复制代码for (MeetingRecord meetingRecord : MeetingRecords) {
// 到其他应用获取某些信息,好家伙!!!
getVirtualRoomById(meetingRecord.getId());
}

当时就想看看这里的耗时有多久,就用arthas的trace命令看下

2.3 arthas

trace class-pattern method-pattern 查看方法内部调用路径,并输出方法路径上的每个节点上耗时

image.png

这次请求的循环体里,耗时最小的一次调用为7ms,最大1069ms,一共22秒。

image.png

2.4 解决

知道原因就好办了,改成批量查询就好,再用arthas看看

image.png

好的,这个大头解决了。

不过时延还没达标,平均时延虽然降到了300ms,但是还没达标呢,只能继续优化

苦笑.png

  1. 寻找系统瓶颈(SQL优化)

还是老样子,先用top命令看下CPU的使用率

3.1 top

image.png

好了,这时候看到CPU的使用率还挺正常的,继续用jstack命令分析

3.2 jstack

继续用jstack -l Java进程的PID > stack.txt分析,发现还是有挺多线程阻塞在java.net.SocketInputStream.socketRead0(Native Method)上,不过这次情况不一样,这些阻塞是由两条SQL引起的,我第一反应是不是慢查询,就赶紧把这两条SQL拿出来,用explain分析一下

image.png

3.3 explain

SQL1 : select * from meeting_record where staff_id = XXX and created_date between XXXX and XXXX,根据员工id和时间,范围检索会议记录

SQL 2: select * from meeting_member mm left join meeting m on mm.meeting_id = m.id where mm.meeting_id = XXXX,根据会议id,获取参会人(这里用了外键,所以SQL里面有left join)

3.3.1 SQL1分析

用explain去分析SQL1

select * from meeting_record where staff_id = XXX and created_date between XXXX and XXXX

1
2
3
4
5
table复制代码+----+-------------+----------------+------------+------+-------------------------------+-------------------------------+---------+-------------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+------------+------+-------------------------------+-------------------------------+---------+-------------+------+----------+-----------------------+
| 1 | SIMPLE | meeting_record | NULL | ref | idx_staff_id_and_created_date | idx_staff_id_and_created_date | 22 | const,const | 1 | 100.00 | Using index condition |
+----+-------------+----------------+------------+------+-------------------------------+-------------------------------+---------+-------------+------+----------+-----------------------+
  • type: ref,代表使用普通索引与某个值相比较,可能会找符合条件的多个数据行
  • possible_type: 可能使用的索引
  • key: idx_staff_id,表示用的是staff_id这个索引
  • ref: const,表示与索引列匹配的值是一个常数
  • Extra: Using index condition,这里的索引是(staff_id,created_date),但是实际type的类型是ref,表示索引只是用了staff_id与某个值进行比较,没有用到created_date。 Extra为Using index condition表示将满足staff_id条件的索引项再用created_date去过滤,然后再回表获取所需要的字段

Using index condition: 索引下推,在5.6版本之后,筛选出索引记录后,再通过where中不满足索引检索的条件进行过滤,再回表查询。 这样的好处是省去很多回表操作,减少随机IO

3.3.2 SQL2分析

select * from meeting_member mm left join meeting m on mm.meeting_id = m.id where mm.meeting_id = XXXX

1
2
3
4
5
6
table复制代码+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------------------------+------+----------+-------+
| 1 | SIMPLE | mm | NULL | ref | idx_mno | idx_mno | 131 | const | 1 | 100.00 | NULL |
| 1 | SIMPLE | m | NULL | eq_ref | PRIMARY | PRIMARY | 130 | mindlinker_meeting_dev.mm.meeting_no | 1 | 100.00 | NULL |
+----+-------------+-------+------------+--------+---------------+---------+---------+--------------------------------------+------+----------+-------+

这里没什么好讲的,唯一的问题是用了外键,老代码了,我也不知道当初为什么要用外键

分析下来,这两条SQL都不是慢查询,索引都用上了,看来不是这里的问题

3.4 jstat

既然SQL没问题,CPU利用率也合理,那么就看看内存吧

用jstat -gc PID 1000打印每秒GC的情况,发现的确很不对劲,一秒两三次YGC,十秒一次FGC

image.png

3.5 对象大小

结合上面的情况,我就猜是不是那两条SQL产生的对象是不是太大了,导致频繁YGC呢,算一算吧

  • MarkWord: 8字节
  • 类型指针: 4字节(开了类型指针压缩-XX:+UseCompressedClassPointer)
  • 普通对象指针: 4字节(开了对象指针压缩 -XX:+UserCompressedOop)
  • 对齐填充:JVM是在内存里是以8个字节为单位进行读写的,所以当对象的大小不是8的整数倍时,需要填充
1
2
3
4
5
6
7
8
9
java复制代码// MarkWord + 类型指针 + 属性大小 + 对齐填充
// 8 + 4 + 80 + 4 = 96 字节
public class MeetingRecord {
private long id; // 8字节
private String meetingNo; // 4字节
private String staffId; // 4字节
private Date createdDate; // 4字节
... // 把上面也加上一共80个字节
}
1
2
3
4
5
6
java复制代码// MarkWord + 类型指针 + 属性大小 + 对齐填充
// 8 + 4 + 124 + 0 = 136 字节
public class Meeting {
private String id;
... // 一共124个字节
}
1
2
3
4
5
6
7
java复制代码// MarkWord + 类型指针 + 属性大小 + 对齐填充
// 8 + 4 + 72 + 4 = 88 字节
public class MeetingMember {
private long id;
private String meetingNo;
... // 一共72个字节
}

按实体的关系,MeetingRecord : MeetingMember : Meeting = 1 : 10 : 1

但是这里有个问题,就是Meeting和MeetingMember用外键进行关联了,所以这个场景下的对象比例为

MeetingRecord : MeetingMember : Meeting = 1 : 10 : (1 + 10)

我是按200场会议,每场会议十个参会人来压测的,所以一个请求这两条SQL能产生的对象总大小约为

1
2
3
ini复制代码  MeetingRecord * 200 + MeetingMember * 200 * 10 +  Meeting * 200 * (10 + 1)
= 96 * 200 + 88 * 200 * 10 + 136 * 200 * 11
= 494400 B = 482 KB

好家伙,两条SQL给我整个482KB,优化SQL去。

指针压缩:由于Java对象的对齐填充机制,这就会导致Java对象的大小都会是8字节的整数倍。基于这种情况,JVM就将堆内存进行了块划分,以8字节为最小单位进行划分,类似操作系统的内存分页机制。因此指针地址就可以不用存对象的真实的64位地址了,而是可以存一个映射地址编号

3.6 优化SQL

3.6.1 使用具体字段+覆盖索引

结合业务,其实MeetingRecord只需要获取Meeting的id即可

1
2
3
4
5
csharp复制代码// 优化前
select * from meeting_record where staff_id = XXX and created_date between XXXX and XXXX

// 返回实际用到的值
select meeting_no from meeting_record where staff_id = XXX and created_date between XXXX and XXXX

由于这里只需要用到meeting_no这个字段,为了减少回表带来的随机IO,我们还可以利用覆盖索引来进行优化

1
2
3
4
5
6
sql复制代码// Extra为Using index condition
select meeting_no from meeting_record where staff_id = XXX and created_date between XXXX and XXXX

// 覆盖索引,把meeting_no增加到联合索引上
// 变成(staff_id,created_date,meeting_no)
select meeting_no from meeting_record where staff_id = XXX and created_date between XXXX and XXXX

再来用explain看看优化之后的情况

1
2
3
4
5
sql复制代码+----+-------------+----------------+------------+-------+------------------------------+------------------------------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------------+------------+-------+------------------------------+------------------------------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | meeting_record | NULL | range | idx_staff_created_meeting_no | idx_staff_created_meeting_no | 22 | NULL | 1 | 100.00 | Using where; Using index |
+----+-------------+----------------+------------+-------+------------------------------+------------------------------+---------+------+------+----------+--------------------------+
  • type: range表示索引使用了范围扫描
  • Extra: Using where; Using index 表示先用staff_id进行检索,然后用where中的created_date进行索引的范围过滤,再获取meeting_no,因为索引上已经包含了meeting_no这个值,所以就不需要回表了

优化完毕

3.6.2 去除外键

1
2
3
4
5
6
7
8
csharp复制代码// 优化前
select * from meeting_member mm left join meeting m on mm.meeting_id = m.id where mm.meeting_id = XXXX

// 去除外键
select * from meeting_member where meeting_no = XXX

// 返回具体的字段
select name,avatar,status... from meeting_member where meeting_no = XXX

3.7 优化后的情况

3.7.1 对象大小

MeetingRecord没了,因为只返回了MeetingRecord中的MeetingID

Meeting : MeetingMember = 1 : 10

1
2
3
ini复制代码  Meeting * 200 + MeetingMember * 200 * 10
= 136 * 200 + 88 * 200 * 10
= 203200 B = 198 KB

比优化前要少个284 KB

3.7.2 GC情况

image.png

看上去正常多了,而且改到这里时,时延也达标了

  1. 额外优化-TLAB

由于这个接口一次请求所产生的对象要占136KB以上的堆空间,就想到这些对象在Eden区分配的情况会是怎样的呢

因为Eden区是线程共享的,当多个线程一起在Eden区分配对象时,会出现线程安全的问题,因此需要加锁分配。为了提高分配效率,每个线程有自己的分配缓冲区(TLAB)来避免和减少使用锁,从而实现快速分配

4.1 对象分配的流程

image.png

TLAB剩余空间的阈值这个可以通过TLABRefillWasteFraction来调整,默认为64,指的是TLAB中浪费空间和TLAB块的比例,也就是说100KB大小的TLAB会浪费 100 / 64 = 1.5KB。

这里我只关注绿色的部分,还有些地方没有画全,比如说分配新TLAB失败怎么办,慢速分配失败怎么办

4.2 TLAB的使用情况

用-XX:+PrintTLAB查看TLAB的使用情况

image.png

重点看这个

1
arduino复制代码TLAB totals: thrds: 189  refills: 8959 max: 199 slow allocs: 676 max 34 waste:  1.1% gc: 3474216B max: 58648B slow: 2512B max: 344B fast: 282288B max: 5560B
  • thrds: 总共有多少条线程使用了TLAB(189条线程)
  • refills: 上次GC到这次GC期间,分配新的TLAB的次数(8959次)
  • max: 单个线程中,分配新的TLAB的最大次数(199次)
  • slow allocs: 所有线程的对象慢速分配的次数(676次)
  • max: 单个线程中,对象慢速分配的最大次数(34次)
  • waste: 所有线程中TLAB的内存浪费比例(1.1%)
  • gc: 发生GC时,所有线程还没使用的TLAB空间
  • slow: 产生新的TLAB时,旧的TLAB浪费的空间
  • fast: 指在C1中,产生新的TLAB时,旧的TLAB浪费的空间

可以看到,从上一次GC到这次GC其间,重新分配了TLAB的次数达到了9000次,慢分配也有个676次。

4.3 申请TLAB和对象慢速分配的流程

因为申请新的TLAB和对象慢速分配,都需要在Eden区上操作,这时候就要考虑并发的问题了。

申请TLAB分区和对象慢分配流程图:
image.png

4.4 优化TLAB

4.4.1 优化前的TLAB情况

我压测的机器跟线上的机器配置差不多,都是2GB的内存,堆空间为1280MB,根据默认的JVM配置,年轻代的大小就是426MB,Eden区就是340MB

TLAB的总大小默认占Eden区的1%,也就是所有TLAB加起来为的空间为3481KB,按上面的线程来看,平均每条线程能分得的TLAB大小为18KB,比我之前算的两条SQL产生的对象大小还小。

4.4.2 调整TLAB大小

假设每条线程能获得200KB的TLAB,那么200条线程就是39MB,也就是说TLAB总大小要占Eden区的8%左右

那么就可以设置JVM参数-XX:TLABWasteTargetPercent=8来调整

看看效果

image.png

1
arduino复制代码TLAB totals: thrds: 121  refills: 500 max: 11 slow allocs: 5 max 1 waste: 12.2% gc: 43304504B max: 8907064B slow: 16000B max: 15536B fast: 356856B max: 54400B

好多了,refills从9000降到了500,慢速分配从670次降到了5次,不过waste从1.1%上升到了12.2%

因为不是所有线程的TLAB都需要200KB这么大,所以某些线程的TLAB在上一次GC到这次GC其间,可能只用了几十KB,导致GC时TLAB剩下的空间都浪费了。所以waste的值也相应地上去了。所以调整这个TLABWasteTargetPercent这个值需要折中,虽然分配效率提升了,但是相应地内存碎片、空间浪费的问题也更明显了

  1. 额外补充

  • 垃圾收集器

我选用的是CMS,因为CMS是一种以获取最短回收停顿时间为目标的收集器,符合我优化的目标。 那为毛不选G1呢? 主要是因为内存太小了,G1官方推荐使用的内存大小是6GB,而我只有2GB,如果使用G1的话,分区会很小,从而导致分配效率低下

  • 业务逻辑优化

其实业务层还有很多地方优化,比如用limit来实现分页,防止一次查询过多的数据。不过这些改动需要客户端配合,就暂时没动

  • 为什么不用缓存

主要是因为这部分逻辑都是旧代码,没有单元测试保证,不敢乱来,加上时间方面以及影响范围等各方面的考虑,决定采用最小改动

好了,完结撒花


参考

  • GC参考手册-Java版
  • 深入理解Java虚拟机
  • JVM G1源码分析和调优

本文转载自: 掘金

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

盘点 TCC-Transaction Debug 手册

发表于 2021-06-03

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

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

文章目的 :

  • 基于 Feign 的 TCC-Transaction 案例
  • 梳理 TCC-Transaction 的 主流程及 Debug 方向

TCC-Transaction 背景简介 :

TCC 模型

TCC 模型是完全依赖于业务处理的分布式事务模型 , 他将一个 [大事务] 通过代码逻辑分解为 [多个小事务] , TCC 模型同样是 2PC (两阶段提交) 的实现之一.

TCC 的操作可以分为3个阶段 : Try / Confirm / Cancel

  • Try: 业务的主要处理 , 但仅进行初期操作 (例如订单生成)
    • 尝试执行业务
    • 所有子事务完成所有业务检查(一致性)
    • 锁定资源 , 预留必须业务资源(准隔离性)
  • Confirm: 对 Try 操作的一个补充,逐个执行Try操作指定的Confirm操作
    • 真正执行业务,不作任何业务检查
    • 只使用Try阶段预留的业务资源 , 扣除具体的资源
    • Confirm 操作满足幂等性
  • Cancel: 对Try操作的一个回撤
    • 取消执行业务
    • 释放Try阶段预留的业务资源, 业务上的回退
    • Cancel操作满足幂等性

TCC Module.jpg

TCC 的优缺点

优点:

  • 由业务方自行控制事务的范围
  • 自如的控制数据库粒度处理 , 降低锁冲突
  • 业务设计合理 , 可以大大提高吞吐量
  • 代码配置简单 , 无需太多配置 , 集成方式便利

缺点 :

  • 业务侵入大 , 耦合强 , 迁移及改造成本大
  • 设计难度大
  • 对于回滚的处理困难
  • 为了满足一致性的要求,confirm和cancel接口必须实现幂等

二 . TCC-Transaction 案例

官方提供过一个基于 Dubbo 的处理案例 , 本案例是基于 Feign 进行 RestAPI 调用的 , 其核心原理其实是一致的.

为了后文分析时更加清楚 , 首先看一下案例源码 :

业务模块 :

  • Order : 订单服务
  • Capital : 账户服务
  • RedPacket : 红包服务

订单支付后 , 扣除账户余额和红包 . 当红包余额不足时 , 发起回退

前期配置

TODO : TCC 的灵活配置

2.1 Order 服务

Order 主流程 : 发起整个事务及接口调用逻辑

  • makePayment : 生成 Order 订单 , 调用红包及账户服务扣除余额
  • confirmMakePayment
  • cancelMakePayment : 确定后修改状态
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
java复制代码@Service
public class PaymentServiceImpl {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired
TradeOrderServiceProxy tradeOrderServiceProxy;

@Autowired
OrderRepository orderRepository;


/**
* 此处考虑后应该是要去掉小事务管理的的 (@Transaction) <br>
* 原因 : 如果此处存在事务 , 分布式应用上抛出异常 , 则回导致该事务回滚
* <p>
* 但是!!! 你可能这个时候会想 , 既然出现异常这个类会回滚 , 那不相当于分布式实现了吗 , 为什么还要加个框架处理
* <p>
* 原因 : 此处如果考虑红包的逻辑就对了 , 这个场景实际上为 : 余额足够 ,但是红包不够的情况!!!
*
* @param order
* @param redPacketPayAmount
* @param capitalPayAmount
*/
@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment", asyncConfirm = true)
public void makePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {


logger.info("order try make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
logger.info("------> 开始事务管理 : 红包支付金额 [{}] / 账户支付余额 [{}] <-------", redPacketPayAmount, capitalPayAmount);
//check if the order status is DRAFT, if no, means that another call makePayment for the same order happened, ignore this call makePayment.
if (order.getStatus().equals("DRAFT")) {
order.pay(redPacketPayAmount, capitalPayAmount);
try {
orderRepository.updateOrder(order);
} catch (OptimisticLockingFailureException e) {
logger.error("E----> error :{} -- content :{}", e.getClass(), e.getMessage());
}
}

logger.info("------> 订单处理完成 :[{}] <-------", order.getId());
String result = tradeOrderServiceProxy.record(null, buildCapitalTradeOrderDto(order));
logger.info("------> 余额消费完成 :[{}] <-------", result);
String result2 = tradeOrderServiceProxy.record(null, buildRedPacketTradeOrderDto(order));
logger.info("------> 红包消费完成 :[{}] <-------", result2);
}

public void confirmMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {

logger.warn("------> [进入 PayConfirm 流程] <-------");
logger.warn("order confirm make payment called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

Order foundOrder = orderRepository.findByMerchantOrderNo(order.getMerchantOrderNo());

//check order status, only if the status equals DRAFT, then confirm order
if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) {
order.confirm();
orderRepository.updateOrder(order);
}
}

public void cancelMakePayment(Order order, BigDecimal redPacketPayAmount, BigDecimal capitalPayAmount) {

logger.error("------> [进入 cancel 流程] <-------");
logger.error("order cancel make payment called.time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

Order foundOrder = orderRepository.findByMerchantOrderNo(order.getMerchantOrderNo());
logger.info("------> [cancel 流程处理 Order :{}] <-------", JSONObject.toJSONString(foundOrder));

if (foundOrder != null && foundOrder.getStatus().equals("PAYING")) {
order.cancelPayment();
orderRepository.updateOrder(order);
}
}


private CapitalTradeOrderDto buildCapitalTradeOrderDto(Order order) {

CapitalTradeOrderDto tradeOrderDto = new CapitalTradeOrderDto();
tradeOrderDto.setAmount(order.getCapitalPayAmount());
tradeOrderDto.setMerchantOrderNo(order.getMerchantOrderNo());
tradeOrderDto.setSelfUserId(order.getPayerUserId());
tradeOrderDto.setOppositeUserId(order.getPayeeUserId());
tradeOrderDto.setOrderTitle(String.format("order no:%s", order.getMerchantOrderNo()));

return tradeOrderDto;
}

private RedPacketTradeOrderDto buildRedPacketTradeOrderDto(Order order) {
RedPacketTradeOrderDto tradeOrderDto = new RedPacketTradeOrderDto();
tradeOrderDto.setAmount(order.getRedPacketPayAmount());
tradeOrderDto.setMerchantOrderNo(order.getMerchantOrderNo());
tradeOrderDto.setSelfUserId(order.getPayerUserId());
tradeOrderDto.setOppositeUserId(order.getPayeeUserId());
tradeOrderDto.setOrderTitle(String.format("order no:%s", order.getMerchantOrderNo()));

return tradeOrderDto;
}
}

Capital , Red 远程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Component
public class TradeOrderServiceProxy {

@Autowired
CapitalTradeOrderService capitalTradeOrderService;

@Autowired
RedPacketTradeOrderService redPacketTradeOrderService;

@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class)
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
return capitalTradeOrderService.record(new TransactionEntity<>(transactionContext, tradeOrderDto));
}

@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class)
public String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto) {
return redPacketTradeOrderService.record(new TransactionEntity<>(transactionContext, tradeOrderDto));
}
}

2.2 Capital 账户处理余额

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
java复制代码@Service
public class CapitalTradeOrderServiceImpl {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired
CapitalAccountRepository capitalAccountRepository;

@Autowired
TradeOrderRepository tradeOrderRepository;

/**
* Step 1 : @Transactional 保证小事务的执行 , 避免余额反复添加
*
* @param transactionContext
* @param tradeOrderDto
* @return
*/
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {

logger.info("capital try record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

TradeOrder foundTradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

//check if trade order has been recorded, if yes, return success directly.
if (foundTradeOrder == null) {

TradeOrder tradeOrder = new TradeOrder(
tradeOrderDto.getSelfUserId(),
tradeOrderDto.getOppositeUserId(),
tradeOrderDto.getMerchantOrderNo(),
tradeOrderDto.getAmount()
);

try {
tradeOrderRepository.insert(tradeOrder);

CapitalAccount transferFromAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

transferFromAccount.transferFrom(tradeOrderDto.getAmount());

capitalAccountRepository.save(transferFromAccount);

logger.info("------> 账户余额处理完成 , 现余额 [{}] <-------", JSONObject.toJSONString(transferFromAccount));

} catch (DataIntegrityViolationException e) {
logger.error("E----> error :{} -- content :{}", e.getClass(), e.getMessage());
}
}

return "success";
}

@Transactional
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {

logger.warn("capital confirm record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

//check if the trade order status is DRAFT, if yes, return directly, ensure idempotency.
if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
tradeOrder.confirm();
tradeOrderRepository.update(tradeOrder);

CapitalAccount transferToAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());

transferToAccount.transferTo(tradeOrderDto.getAmount());

capitalAccountRepository.save(transferToAccount);
}
}

@Transactional
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {

logger.error("capital cancel record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));
TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

//check if the trade order status is DRAFT, if yes, return directly, ensure idempotency.
if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
tradeOrder.cancel();
tradeOrderRepository.update(tradeOrder);

CapitalAccount capitalAccount = capitalAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

capitalAccount.cancelTransfer(tradeOrderDto.getAmount());

capitalAccountRepository.save(capitalAccount);
}
}
}

2.3 RedPacket 红包处理

  • record : 创建红包订单 , 扣除金额
  • confirmRecord : 更新订单状态 , 扣除账户
  • cancelRecord :
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
java复制代码@Service
public class RedPacketTradeOrderServiceImpl {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired
RedPacketAccountRepository redPacketAccountRepository;

@Autowired
TradeOrderRepository tradeOrderRepository;

@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class)
@Transactional
public String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto) {

logger.info("red packet try record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

TradeOrder foundTradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

if (foundTradeOrder == null) {

TradeOrder tradeOrder = new TradeOrder(
tradeOrderDto.getSelfUserId(),
tradeOrderDto.getOppositeUserId(),
tradeOrderDto.getMerchantOrderNo(),
tradeOrderDto.getAmount()
);

try {
tradeOrderRepository.insert(tradeOrder);

RedPacketAccount transferFromAccount = redPacketAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

transferFromAccount.transferFrom(tradeOrderDto.getAmount());

redPacketAccountRepository.save(transferFromAccount);

logger.info("------> [] <-------");

} catch (DataIntegrityViolationException e) {
logger.error("E----> error :{} -- content :{}", e.getClass(), e.getMessage());
}
}

return "success";
}

@Transactional
public void confirmRecord(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto) {

logger.warn("red packet confirm record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
tradeOrder.confirm();
tradeOrderRepository.update(tradeOrder);

RedPacketAccount transferToAccount = redPacketAccountRepository.findByUserId(tradeOrderDto.getOppositeUserId());

transferToAccount.transferTo(tradeOrderDto.getAmount());

redPacketAccountRepository.save(transferToAccount);
}
}

@Transactional
public void cancelRecord(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto) {

logger.error("red packet cancel record called. time seq:" + DateFormatUtils.format(Calendar.getInstance(), "yyyy-MM-dd HH:mm:ss"));

TradeOrder tradeOrder = tradeOrderRepository.findByMerchantOrderNo(tradeOrderDto.getMerchantOrderNo());

if (null != tradeOrder && "DRAFT".equals(tradeOrder.getStatus())) {
tradeOrder.cancel();
tradeOrderRepository.update(tradeOrder);

RedPacketAccount capitalAccount = redPacketAccountRepository.findByUserId(tradeOrderDto.getSelfUserId());

capitalAccount.cancelTransfer(tradeOrderDto.getAmount());

redPacketAccountRepository.save(capitalAccount);
}
}
}

2.4 流程总结

流程源码可以看 @项目源码

三 . 源码分析

3.1 TCC 成员梳理

  • 事务 : ( Transaction )
  • 事务ID对象 : ( TransactionXid )
  • 事务状态对象 : ( TransactionStatus )
  • 事务类型对象 : ( TransactionType )

  • 参与者 : ( org.mengyun.tcctransaction.Participant )
  • 事务管理器 : TransactionManager
  • 事务恢复配置器 : RecoverConfig
  • 事务恢复处理器 : TransactionRecovery
  • 默认事务恢复配置实现 : DefaultRecoverConfig
  • 事务恢复定时任务 : RecoverScheduledJob
  • 事务恢复处理器 : TransactionRecovery
  • 事务恢复处理器 : TransactionRecovery

3.2 TCC 流程快查

事务的主要流程如下所示 :

  • 发起根事务 : MethodType.ROOT / begin / registerTransaction
  • 传播发起分支事务 : MethodType.PROVIDER / try / propagationNewBegin
  • 传播获取分支事务 : MethodType.PROVIDER / confirm / cancel / propagationExistBegin
  • 提交事务 : commit / confirm / cancel /
  • 回滚事务 : rollback / confirm / cancel /
  • 添加事务 : enlistParticipant / try
  • 事务拦截器 : @Compensable / @Aspect / @Transactional

3.3 流程一 : 切面的处理

TCC 的注解基于 @Aspect + @Compensable 实现切面的处理 , 核心的处理类为 CompensableTransactionAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Aspect
public abstract class CompensableTransactionAspect {

private CompensableTransactionInterceptor compensableTransactionInterceptor;

public void setCompensableTransactionInterceptor(CompensableTransactionInterceptor compensableTransactionInterceptor) {
this.compensableTransactionInterceptor = compensableTransactionInterceptor;
}

@Pointcut("@annotation(org.mengyun.tcctransaction.api.Compensable)")
public void compensableService() {

}

// 可以看到 , 这里使用的环绕切面
@Around("compensableService()")
public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {

return compensableTransactionInterceptor.interceptCompensableMethod(pjp);
}

public abstract int getOrder();
}

处理拦截器

当切面拦截后 , 此处会使用拦截器进行真正的具体逻辑 :

  • org.mengyun.tcctransaction.interceptor.CompensableTransactionInterceptor,可补偿事务拦截器。
  • org.mengyun.tcctransaction.interceptor.ResourceCoordinatorInterceptor,资源协调者拦截器。
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
java复制代码C05- CompensableTransactionInterceptor
M05_01- interceptCompensableMethod
M05_02- rootMethodProceed

/**
*
*
**/
public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {

// 获取对应得 Method 对象
Method method = CompensableMethodUtils.getCompensableMethod(pjp);

Compensable compensable = method.getAnnotation(Compensable.class);
Propagation propagation = compensable.propagation();

// 获取事务容器
TransactionContext transactionContext = FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().get(pjp.getTarget(), method, pjp.getArgs());

// 是否异步处理
boolean asyncConfirm = compensable.asyncConfirm();
boolean asyncCancel = compensable.asyncCancel();

// 是否开启事务
boolean isTransactionActive = transactionManager.isTransactionActive();

if (!TransactionUtils.isLegalTransactionContext(isTransactionActive, propagation, transactionContext)) {
throw new SystemException("no active compensable transaction while propagation is mandatory for method " + method.getName());
}

//
MethodType methodType = CompensableMethodUtils.calculateMethodType(propagation, isTransactionActive, transactionContext);

switch (methodType) {
case ROOT:
return rootMethodProceed(pjp, asyncConfirm, asyncCancel);
case PROVIDER:
return providerMethodProceed(pjp, transactionContext, asyncConfirm, asyncCancel);
default:
return pjp.proceed();
}
}


// 此处会通过事务的类型选择不同的方法进行处理 :
private Object rootMethodProceed(ProceedingJoinPoint pjp, boolean asyncConfirm, boolean asyncCancel) throws Throwable {

// 属性准备
Object returnValue = null;
Transaction transaction = null;

try {
// 开启事务 , 获取事务对象 -> M05_02_01
transaction = transactionManager.begin();
try {
// 执行 proceed 处理方法
returnValue = pjp.proceed();
} catch (Throwable tryingException) {

if (!isDelayCancelException(tryingException)) {
transactionManager.rollback(asyncCancel);
}
throw tryingException;
}
// 事件管理器提交 commit
transactionManager.commit(asyncConfirm);
} finally {
transactionManager.cleanAfterCompletion(transaction);
}
return returnValue;
}

Pro 1 : Propagation 是什么 ?

Propagation 提供了多种传播方式 , 来定义具体的传播类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3);
//.............
}


// @Transactional(propagation=Propagation.REQUIRED)
- 如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)

// @Transactional(propagation=Propagation.REQUIRES_NEW)
- 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务

// @Transactional(propagation=Propagation.MANDATORY)
- 必须在一个已有的事务中执行,否则抛出异常

// @Transactional(propagation=Propagation.SUPPORTS)
- 如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务

Pro 2 : MethodType 是什么

MethodType 表示方法对应的事务类型 :

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码

public enum MethodType {
ROOT,
CONSUMER,
PROVIDER,
NORMAL;
}

- MethodType.ROOT : 根事务 , 也可以理解为事务发起者
- MethodType.CONSUMER : 消费参与者
- MethodType.PROVIDER : 生产参与者
- MethodType.NORMAL :

M05_02_01 Transaction 实体类解析

tcc-trans-transMangerBean.jpg

3.4 事务的执行和通知

3.4.1 事务的commit

之前 C05- CompensableTransactionInterceptor # M05_02- rootMethodProceed 中通过 TransactionManager 执行 commit 操作

先看一下 TransactionManager 的结构 :

1
2
3
4
5
6
7
8
9
java复制代码C09- TransactionManager
F09_01- ThreadLocal<Deque<Transaction>> CURRENT = new ThreadLocal<Deque<Transaction>>();
M09_01- begin()
M09_02- propagationNewBegin(TransactionContext transactionContext)
M09_03- propagationExistBegin(TransactionContext transactionContext)
M09_04- commit(boolean asyncCommit)
M09_05- rollback(boolean asyncRollback)
M09_06- commitTransaction(Transaction transaction)
M09_07- rollbackTransaction(Transaction transaction)

Step 1 : TransactionManager 进行管理

TransactionManager 进行 Transaction 的配合和通过线程发起事务

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
java复制代码
public void commit(boolean asyncCommit) {
// 从 ThreadLocal 中获取 Transaction
final Transaction transaction = getCurrentTransaction();
// commit 后修改状态
transaction.changeStatus(TransactionStatus.CONFIRMING);
// 更新 Transaction 状态
transactionRepository.update(transaction);

if (asyncCommit) {
try {
Long statTime = System.currentTimeMillis();

// 此处主要为 ThreadPoolExecutor , 通过线程池提交 -> M09_06
executorService.submit(new Runnable() {
@Override
public void run() {
commitTransaction(transaction);
}
});
} catch (Throwable commitException) {
throw new ConfirmingException(commitException);
}
} else {
commitTransaction(transaction);
}
}

Step 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码// M09_06 此处提交事务
private void commitTransaction(Transaction transaction) {
try {
// 提交多个事务
transaction.commit();
// 删除事务
transactionRepository.delete(transaction);
} catch (Throwable commitException) {
logger.warn("compensable transaction confirm failed, recovery job will try to confirm later.", commitException);
throw new ConfirmingException(commitException);
}
}

// C15- Transaction -> PS:C15_01
public void commit() {
for (Participant participant : participants) {
participant.commit();
}
}

public void commit() {
terminator.invoke(new TransactionContext(xid, TransactionStatus.CONFIRMING.getId()), confirmInvocationContext, transactionContextEditorClass);
}


public Object invoke(TransactionContext transactionContext, InvocationContext invocationContext, Class<? extends TransactionContextEditor> transactionContextEditorClass) {
if (StringUtils.isNotEmpty(invocationContext.getMethodName())) {
try {
Object target = FactoryBuilder.factoryOf(invocationContext.getTargetClass()).getInstance();
Method method = null;
// 代理执行对象的 Method
method = target.getClass().getMethod(invocationContext.getMethodName(), invocationContext.getParameterTypes());
FactoryBuilder.factoryOf(transactionContextEditorClass).getInstance().set(transactionContext, target, method, invocationContext.getArgs());
return method.invoke(target, invocationContext.getArgs());
} catch (Exception e) {
throw new SystemException(e);
}
}
return null;
}


// public final void com.tcc.demo.order.service.PaymentServiceImpl$$EnhancerBySpringCGLIB$$6a17cffa.confirmMakePayment(com.tcc.demo.order.model.Order,java.math.BigDecimal,java.math.BigDecimal)



// TransactionRepository 是用于事务管理的持久化操作
C10- TransactionRepository -> PS:C10_01


// ExecutorService 执行 Service
C12- ExecutorService -> PS:C11_01

PS:C10_01 TransactionRepository 家族体系

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 TransactionRepository {
int create(Transaction transaction);
int update(Transaction transaction);
int delete(Transaction transaction);
Transaction findByXid(TransactionXid xid);
List<Transaction> findAllUnmodifiedSince(Date date);
}


// 这里看一下 Transaction 的数据库结构

CREATE TABLE `TCC_TRANSACTION` (
`TRANSACTION_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '事务 ID',
`DOMAIN` varchar(100) DEFAULT NULL COMMENT '域名',
`GLOBAL_TX_ID` varbinary(32) NOT NULL COMMENT '全局事务ID',
`BRANCH_QUALIFIER` varbinary(32) NOT NULL,
`CONTENT` varbinary(8000) DEFAULT NULL COMMENT '序列化 Transaction 事务内容',
`STATUS` int(11) DEFAULT NULL COMMENT '事务状态',
`TRANSACTION_TYPE` int(11) DEFAULT NULL COMMENT '事务类型',
`RETRIED_COUNT` int(11) DEFAULT NULL COMMENT '重试次数',
`CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间',
`LAST_UPDATE_TIME` datetime DEFAULT NULL COMMENT '最后更新时间',
`VERSION` int(11) DEFAULT NULL COMMENT '乐观锁版本',
`IS_DELETE` int(12) DEFAULT NULL COMMENT '是否删除',
PRIMARY KEY (`TRANSACTION_ID`),
UNIQUE KEY `UX_TX_BQ` (`GLOBAL_TX_ID`,`BRANCH_QUALIFIER`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


// 每一个小事务都有个自己的 TCC_TRANSACTION 类 , 类名通常为 TCC_TRANSACTION_xxx

TransactionRepository_system.png

PS:C15_01 参数

tcc-participants.jpg

Pro 1 : Xid 的对象

1
2
3
4
5
6
7
8
9
10
java复制代码public interface Xid {
int MAXGTRIDSIZE = 64;
int MAXBQUALSIZE = 64;

int getFormatId();

byte[] getGlobalTransactionId();

byte[] getBranchQualifier();
}

3.4.2 事务的通知

事务的还有一个核心逻辑就是通知其他的应用执行相关的逻辑 , 那么事务是怎么相互告知的呢 ? 我们从实际应用出发 :

问题 :

疑点一 : 当 captial try 逻辑完成后 , 实际上已经返回了 , 并不会拿到对应的通知

现象 :

现象一 : 当 try 再次调用时 , 是通过 restAPI 接口进行网络调用 , 所以应该是外部调用实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// 找了相关的代码找到了这个类 : 

// C15- Transaction -> PS:C15_01
public void commit() {
for (Participant participant : participants) {
participant.commit();
}
}

public class TradeOrderServiceProxy {

@Autowired
CapitalTradeOrderService capitalTradeOrderService;

@Autowired
RedPacketTradeOrderService redPacketTradeOrderService;

@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class)
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
return capitalTradeOrderService.record(new TransactionEntity<>(transactionContext, tradeOrderDto));
}

//.............
}

如果我们跟着相关的代码 , 会发现确实在出现rollbakc 时 , 会调用该参与者

所以我们能得出如下的结论 :

  1. 当出现异常后 , 会通过 Participant 调用相同的接口一次
  2. 调用对应接口后 , 会因为拦截器的原因 , 通过 Transaction 状态 , 调用对应的所属流程 (例如异常就是 rollback)

3.5 事务的回退

事务的回退时 , 会先调用起本身的 cancel 方法 ,其次会调用依赖微服务的原方法

PS : 确实是原方法 , 但是由于代理 , 会进入 CompensableTransactionAspect 切面

通过判断 TransactionContext 中的 status 决定执行什么方法

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
java复制代码private Object providerMethodProceed(ProceedingJoinPoint pjp, TransactionContext transactionContext, boolean asyncConfirm, boolean asyncCancel) throws Throwable {

Transaction transaction = null;
try {

switch (TransactionStatus.valueOf(transactionContext.getStatus())) {
case TRYING:
transaction = transactionManager.propagationNewBegin(transactionContext);
return pjp.proceed();
case CONFIRMING:
try {
transaction = transactionManager.propagationExistBegin(transactionContext);
transactionManager.commit(asyncConfirm);
} catch (NoExistedTransactionException excepton) {
//the transaction has been commit,ignore it.
}
break;
case CANCELLING:
//
try {
transaction = transactionManager.propagationExistBegin(transactionContext);
transactionManager.rollback(asyncCancel);
} catch (NoExistedTransactionException exception) {
//the transaction has been rollback,ignore it.
}
break;
}

} finally {
transactionManager.cleanAfterCompletion(transaction);
}

Method method = ((MethodSignature) (pjp.getSignature())).getMethod();

return ReflectionUtils.getNullValue(method.getReturnType());
}

public void rollback() {
for (Participant participant : participants) {
participant.rollback();
}
}


// 回退的发起
public void rollback() {
terminator.invoke(new TransactionContext(xid, TransactionStatus.CANCELLING.getId()), cancelInvocationContext, transactionContextEditorClass);
}



// 注意 :
1- Participant -> Order Cancel
2- Participant -> Capital Rest API
3- Participant -> Capital Cancel

3.6 事务的异步处理

TCC-Transaction 的处理默认是同步的 , 可以通过注解来配置异步处理

@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment", asyncConfirm = false, asyncCancel = false)

这里来看一下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
java复制代码    public void rollback(boolean asyncRollback) {

final Transaction transaction = getCurrentTransaction();
transaction.changeStatus(TransactionStatus.CANCELLING);

transactionRepository.update(transaction);

if (asyncRollback) {

try {
// 最大的区别就是此处调用线程池构建了一个新线程
executorService.submit(new Runnable() {
@Override
public void run() {
rollbackTransaction(transaction);
}
});
} catch (Throwable rollbackException) {
throw new CancellingException(rollbackException);
}
} else {

rollbackTransaction(transaction);
}
}

3.7 事务的恢复

事务的恢复和事务的通知并不是一个概念 , 当事务的初期执行出现异常后 , 事务在后续会通过定时任务的方式 , 完成事务的继续执行操作

  • org.mengyun.tcctransaction.recover.RecoverConfig,事务恢复配置接口
  • org.mengyun.tcctransaction.spring.recover.DefaultRecoverConfig,默认事务恢复配置实现
  • org.mengyun.tcctransaction.spring.recover.RecoverScheduledJob,事务恢复定时任务,基于 Quartz 实现调度,不断不断不断执行事务恢复

总结

正常处理流程 , 执行 Confirm

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
java复制代码A- 事务发起者 (订单服务)
B- 事务消费对象B (账户服务)
C- 事务消费对象C (红包服务)

// Step 1 : 事务发起者处理
A1- 调用发起方法
A2- TCC CompensableTransactionAspect 切面拦截
A3- TCC CompensableTransactionInterceptor # rootMethodProceed 准备事务容器 , 大流程处理
A4- CompensableTransactionInterceptor # rootMethodProceed 开启事务及事务管理 (begin , process , commit)
A5- 进入真正的方法执行业务逻辑

// Step 2 : 账户对象处理
B1- 调用发起方法
B2- TCC CompensableTransactionAspect 切面拦截
B3- TCC CompensableTransactionInterceptor # rootMethodProceed 准备事务容器 , 大流程处理
B4- CompensableTransactionInterceptor # providerMethodProceed 执行消费处理
B5- 进入真正的方法执行业务逻辑

// PS : 主要对比 A4 - B4 的区别

// Step 3 : 账户处理完成后 , 订单服务继续处理
A6- transactionManager.commit(asyncConfirm) : 提交 commit
A7- Transaction.Participant # commit() 提交代理方法
A8- 执行 Order Confirm 方法


// Step 4 : 账户服务Confirm 确定
B6- TransactionAspectSupport # invokeWithinTransaction
B7- 执行 账户 Confirm 方法

回退处理逻辑 , 执行 Cancel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// PS : 前几步都是一样的 , 基于代理的方式


// Step 1: A4 rootMethodProceed 处理过程中出现异常 , 由 catch 继续处理
A4- rootMethodProceed : 执行账户主方法
A5- 出现异常 , catch 处理 , 调用 rollback
A6- TransactionManager # rollback 执行 Rollback 主流程

// Step 2 : order 模块调用 cancel 方法


// Step 3 : 账户对象处理回退
B5- 再次原样调用主方法
B6- 事务容器状态为回退 , 执行回退逻辑

@Compensable 的使用

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复制代码// 整个流程中共有以下几个地方需要标注 @Compensable , 我们来单独看看其中的关联


// Step 1 : Order 中 try 方法标注
@Compensable(confirmMethod = "confirmMakePayment", cancelMethod = "cancelMakePayment", asyncConfirm = true)

// Step 2 : Order 中调用远程接口时标注
@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class)
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {
return capitalTradeOrderService.record(new TransactionEntity<>(transactionContext, tradeOrderDto));
}

@Compensable(propagation = Propagation.SUPPORTS, confirmMethod = "record", cancelMethod = "record", transactionContextEditor = MethodTransactionContextEditor.class)
public String record(TransactionContext transactionContext, RedPacketTradeOrderDto tradeOrderDto) {
return redPacketTradeOrderService.record(new TransactionEntity<>(transactionContext, tradeOrderDto));
}




// Step 3 : capital 中远程接口的 try 方法
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class)

// Step 4 : red 接口中 try 方法
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class)


// PS : 官方原文中 , 此处是没有添加对应方法的 , 不清楚是否是因为 Dubbo 的 Feign 的机制问题

感谢和参考

本文转载自: 掘金

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

Java日期时间API系列37-----时间段是否有重叠(交

发表于 2021-06-02

  
  在日程安排或预约排期等场景中,经常会需要对比2个或多个时间段是重叠的功能,我经过整理和验证,发现了下面的算法比较好一些,分享一下。

1.只有2个时间段的情况

  例如:存在区间A、区间B,重叠的情况很多,但不重叠的情况只有2种,A在B前或者B在A前。如图:

image.png

得出,不重叠算法:A.end< B.start || A.start > B.end

那么重叠的算法对上面取反就可以了:! (A.end< B.start || A.start > B.end)

Java算法实现:! (A.end< B.start || A.start > B.end) 这里为了通用性,将时间类统一通过getTime()方法,转换为时间戳对比。

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
typescript复制代码/**
* 判断2个时间段是否有重叠(交集)
* @param startDate1 时间段1开始时间戳
* @param endDate1 时间段1结束时间戳
* @param startDate2 时间段2开始时间戳
* @param endDate2 时间段2结束时间戳
* @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等,比如2021/5/29-2021/5/31和2021/5/31-2021/6/1,不重叠。
* @return 返回是否重叠
*/
public static boolean isOverlap(long startDate1, long endDate1, long startDate2, long endDate2, boolean isStrict){
if(endDate1<startDate1){
throw new DateTimeException("endDate1不能小于startDate1");
}
if(endDate2<startDate2){
throw new DateTimeException("endDate2不能小于startDate2");
}
if(isStrict){
if(! (endDate1<startDate2 || startDate1>endDate2)){
return true;
}
}else{
if(! (endDate1<=startDate2 || startDate1>=endDate2)){
return true;
}
}
return false;
}

/**
* 判断2个时间段是否有重叠(交集)
* @param startDate1 时间段1开始时间
* @param endDate1 时间段1结束时间
* @param startDate2 时间段2开始时间
* @param endDate2 时间段2结束时间
* @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等,比如2021-05-29到2021-05-31和2021-05-31到2021-06-01,不重叠。
* @return 返回是否重叠
*/
public static boolean isOverlap(Date startDate1, Date endDate1, Date startDate2, Date endDate2, boolean isStrict){
Objects.requireNonNull(startDate1, "startDate1");
Objects.requireNonNull(endDate1, "endDate1");
Objects.requireNonNull(startDate2, "startDate2");
Objects.requireNonNull(endDate2, "endDate2");
return isOverlap(startDate1.getTime(), endDate1.getTime(), startDate2.getTime(), endDate2.getTime(), isStrict);
}

2.大于2个时间段的情况

如果大于2个时间段,需要相互都比较一次,比较麻烦,可以先根据开始时间排序,然后一次遍历对比:

由上面2个时间段算法得出,有序情况下,不重叠算法:A.end< B.start

那么重叠的算法对上面取反就可以了:! (A.end< B.start)

Java算法实现:先根据开始时间排序,遍历对比,! (A.end< B.start)

/**

  • 时间段

*@author xkzhangsan
*/
public class TimePair {

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
arduino复制代码public TimePair(long start, long end) {
if(end<start){
throw new DateTimeException("end不能小于start");
}
this.start = start;
this.end = end;
}

private long start;

private long end;

public long getStart() {
return start;
}

public void setStart(long start) {
this.start = start;
}

public long getEnd() {
return end;
}

public void setEnd(long end) {
this.end = end;
}

}

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
php复制代码/**
* 判断多个时间段是否有重叠(交集)
* @param timePairs 时间段数组
* @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等,比如2021-05-29到2021-05-31和2021-05-31到2021-06-01,不重叠。
* @return 返回是否重叠
*/
public static boolean isOverlap(TimePair[] timePairs, boolean isStrict){
if(timePairs==null || timePairs.length==0){
throw new DateTimeException("timePairs不能为空");
}

Arrays.sort(timePairs, Comparator.comparingLong(TimePair::getStart));

for(int i=1;i<timePairs.length;i++){
if(isStrict){
if(! (timePairs[i-1].getEnd()<timePairs[i].getStart())){
return true;
}
}else{
if(! (timePairs[i-1].getEnd()<=timePairs[i].getStart())){
return true;
}
}
}
return false;
}

/**
* 判断多个时间段是否有重叠(交集)
* @param timePairList 时间段列表
* @param isStrict 是否严格重叠,true 严格,没有任何相交或相等;false 不严格,可以首尾相等,比如2021-05-29到2021-05-31和2021-05-31到2021-06-01,不重叠。
* @return 返回是否重叠
*/
public static boolean isOverlap(List<TimePair> timePairList, boolean isStrict){
if(CollectionUtil.isEmpty(timePairList)){
throw new DateTimeException("timePairList不能为空");
}
TimePair[] timePairs = new TimePair[timePairList.size()];
timePairList.toArray(timePairs);
return isOverlap(timePairs, isStrict);
}

可以看出多个时间段的算法也适用于2个时间段,2个时间段只是其中的一个特例。

地址:www.cnblogs.com/xkzhangsanx…

本文转载自: 掘金

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

MySQL 数据表优化设计(五):如何选择一个合适的时间类型

发表于 2021-06-02

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

MySQL 有多种类型存储日期和时间,例如 YEAR 和 DATE。MySQL 的时间类型存储的精确度能到秒(MariaDB 可以到毫秒级)。但是,也可以通过时间计算达到毫秒级。时间类型的选择没有最佳,而是取决于业务需要如何处理时间的存储。

MySQL 提供了 DATETIME 和 TIMESTAMP 两种非常相似的类型处理日期和时间,大部分情况下两种都是 OK 的,但是有些情况二者会互有优劣。

DATETIME

DATETIME 的时间跨度更大,可以从1001年到9999年,精度是秒。并且存储的格式是将日期和时间打包使用 YYYYMMDDhhmmss格式的整数存储,这个时间与时区无关,需要占用8个字节的存储空间。默认,MySQL 显示 的DATETIME是有序的,明确的格式,例如2021-06-02 18:35:23。这是 ANSI 的标准日期时间格式。

TIMESTAMP

TIMESTAMP即时间戳,存储的是自格林威治时间(GMT)1970年1月1日零点以来的秒数。和 Unix 系统的时间戳一样。TIMESTAMP 仅需要4个字节存储,因此能够表示的时间跨度更小,从1970年到2038年。MySQL 提供了 FROM_UNIXTIME 和 UNIX_TIMESTAMP 函数来完成时间戳和日期之间的转换。

在 MySQL 4.1版本后,TIMESTAMP 显示的格式和 DATETIME 类似,但是,TIMESTAMP 的显示依赖于时区。MySQL 的服务端、操作系统以及客户端连接都有时区的设置。因此,如果时间是从多个时区存储的话,那 TIMESTAMP 和 DATETIME 的差别就会很大。TIMESTAMP 会保留使用时的时区信息,而 DATETIME 仅仅是使用文本表示时间。

TIMESTAMP 还有额外的特性。默认地,MySQL会在没有指定值的情况下使用当前时间插入到 TIMESTAMP列,而更新的时候如果没有指定值会使用当前时间更新该字段,以下面的测试表为例:

1
2
3
4
5
sql复制代码CREATE TABLE t_time_test (
id INT PRIMARY KEY,
t_stamp TIMESTAMP,
t_datetime DATETIME
);

可以看到MySQL 给的默认值就是当前时间戳 CURRENT_TIMESTAMP,并且有个 ON UPDATE CURRENT_TIMESTAMP表示会随之更新:
image.png

1
2
3
4
sql复制代码INSERT INTO t_time_test(id, t_datetime) VALUES
(1, NULL),
(2, '2021-06-02 18:48:04'),
(3, NULL);

可以看到 t_stamp 列自动填充了当前时间。
image.png

1
2
sql复制代码UPDATE `t_time_test` 
SET `t_datetime`='2021-06-02 19:00:00' WHERE id=1;

可以看到 id 为1的一列的 t_stamp 列会自动更新为当前时间。
image.png
这个特性使得我们可以物协程序维护数据更新时间字段,而交由 MySQL 完成。

如何选择

从特性上看,可能会优先选择使用 TIMESTAMP 来存储时间,相比 DATETIME 来说更高效。也有些人使用整数存储 Unix 时间戳,实际上这种方式并不能获益,而且整数还需要额外进行处理,因此并不推荐这么做。但是一些情况需要注意不要使用 TIMESTAMP 存储时间:

  • 生日:生日肯定会有早于1970年的,会超出 TIMESTAMP 的范围
  • 有效期截止时间:TIMESTAMP 的最大时间是2038年,如果用来存类似身份证的有效期截止时间,营业执照的截止时间等就不合适。
  • 业务生存时间:互联网时代讲究快,发展(死得)快。如果要成为长久存在的企业,那么你的业务时间很可能在2038年还在继续运营,毕竟现在都2021年了。如果你觉得公司业务挺不到2038年,那没关系。当然,如果幸运地挺到2038年,请务必写下一条待办事项:到2038年1月1日前修改数据表时间戳字段类型。

如何存储毫秒级时间

通常这个时候需要使用 BIGINT 来将时间转换为整型存储,或者是使用浮点数,用分数部分表示秒精度一下的时间,这两种方式都可行。当然,这个时候需要应用支持做格式转换。

结语

从安全稳妥的角度考虑,建议还是优先选择 DATETIME 类型,虽然相比 TIMESTAMP 会牺牲一点性能,但是 TIMESTAMP 的时间范围是硬伤,不要埋下一个隐患,等到真的2038年,你的公司可能是上市公司的时候,程序员可能会遭遇洪水般的 bug 冲击而不明所以,结果公司的股价迎来闪崩!然后找出来这个程序员,发现是曾经公司的大神,目前的股东,已经实现财务自由的你!你说尴尬不尴尬?

本文转载自: 掘金

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

1…654655656…956

开发者博客

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