关于如何写好代码的一些建议与方法(中)

文章目录

    • 2.6 数据结构与算法
      • 2.6.1 逻辑结构
        • 线性结构
        • 树形结构
        • 图形结构
      • 2.6.2 存储结构
        • 顺序存储
        • 链式存储
      • 2.6.3 算法的魅力
      • 2.6.4 复杂度分析
      • 2.6.5 数组与链表
        • 概念
        • 随机访问
        • 插入、删除
        • 实际场景中需要注意的地方
    • 2.7 异常
      • 2.7.1 异常类型
        • Throwable
        • Error
        • Exception
      • 2.7.2 异常使用的误区
        • 忽视异常
        • 标准化异常
        • 正确的使用异常
        • 让异常保持原子性
      • 2.7.3 其他建议
    • 2.8 事务
      • 2.8.1 事务的传播
        • REQUIRED
        • SUPPORTS
        • MANDATORY
        • REQUIRES\_NEW
        • NOT\_SUPPORTED
        • NEVER
        • NESTED
        • 特别说明
      • 2.8.2 事务应用
        • 编程式事务
        • 事务使用的注意事项
          • 滥用@Transactional
          • 长事务、过早起开事务
          • 锁的粒度
          • 数据库死锁
      • 2.8.3 事务的失效场景
        • 异常未抛出
        • 异常与rollback不匹配
        • 方法内部直接调用
        • 在另一个线程中使用事务
        • 注解作用到private级别的方法上
        • final类型的方法
        • 数据库存储引擎不支持事务
        • 事务的传播类型
    • 2.9 注释
      • 2.9.1 到底该不该写注释
      • 2.9.2 言简意赅
      • 2.9.3 准确性

关于如何写好代码的一些建议与方法(上)
关于如何写好代码的一些建议与方法(中)
关于如何写好代码的一些建议与方法(下)

2.6 数据结构与算法

数据结构一般被分为:逻辑结构与存储结构(内存)

逻辑结构:线性结构、树形结构、图形结构

存储结构:顺序存储、链式存储

2.6.1 逻辑结构

线性结构

线性结构包含:数组、链表、栈、队列,其特点是由一系列数据类型相同的数据元素组成的有序集合。
线性结构的优点是操作简单、速度快,随机访问能力强,但缺点是一旦数据量增加后,要么访问的速度变慢,要么插入或者删除的速度变慢,因此线性结构通常用于处理简单的数据集合,例如Java语言中的List、LinkedList、Queue等容器集合。

树形结构

树这种数据结构就比线性结构要复杂得多了,树一般由多个节点按照层次结构连接而成,常见的树有二叉树、平衡树、红黑树、B树和B+树等,树形结构通常用来解决大数据量之下的数据检索以及操作的问题。

图形结构

图形结构和前两种结构相比,又是一种更复杂的数据结构了,它是由节点和边组成的,节点表示图形中的一个点,边表示两个节点之间的关系。在图形数据结构中,每个节点都有一个唯一的标识符,称为节点ID,而边则用于描述节点之间的关系,如边的权重和边的方向等。

图形数据结构通常用于表示具有网络或连通性的实体或概念,例如社交网络中的好友关系、城市地图等,可以说,凡是多对多的关系,都可以看作是图形结构。

2.6.2 存储结构

顺序存储

顺序存储是将数据按照一定的顺序排列存储在存储器中。这种方式的优点是访问速度快,适合存储结构简单的数据。但是,当需要插入或删除数据时,需要移动后面的数据,时间复杂度较高。

数组、栈、队列、树(树这种结构既可以用顺序存储也可以用链式存储,比如最为典型的就是堆这样的树形结构,它就是用数组来存储的)都可以用顺序存储来完成。

链式存储

链式存储是将数据存储在存储器中的不同位置,每个位置上存储的是一个指针,指向存储在其他位置的数据。这种方式的优点是插入和删除数据的效率高,时间复杂度为O(1)。但是,访问某个数据需要从头开始遍历,时间复杂度较高。

链表、树、图都可以用链式存储来完成。

结合这些数据结构的特点,希望你能在增删改查的场景中进行正确的选择。

2.6.3 算法的魅力

来一道小学生难道的数学题:计算1+2+3+...+100之和

一般玩家会这样做

int sum = 0;
for(int i = 1; i<=100; i++){
    sum += i;
}

高级玩家,会先找到规律,比如:0+100+1+99+2+98+3+97+....+49+51+50
然后根据这样的规律,对代码逻辑进行优化。

int end = 100, half = end / 2, sum;
if ((end & 1) == 0) {
    sum = end * half + half;
} else {
    sum = end * (half + 1);
}

大神玩家,同样是规律,但找到的规律更容易用代码来表现。

1 + 2 + 3 + … + 98 + 99 + 100

100 + 99 + 98 + … + 3 + 2 + 1

int start = 1, end = 100;
int sum = (start + end) * end / 2;

优秀的算法可以有效地降低时间复杂度,后两种解法直接从O(n)降到O(1),如果要加到一万,一千万,一亿,节省的计算量相当可观。

2.6.4 复杂度分析

复杂度一般分为:时间复杂度、空间复杂度。

大多数场景中更注重的是时间复杂度,根据其执行效率,一般又可分为如下几种:

常量阶:O(1)

int i = 1, j = 2;
int sum = i + j;

对数阶:O(logn)

int i = 1;
while(i < n){
    i = i * 2;
}

线性阶:O(n)

int i = 1;
while(i < n){
    i++;
}

线性对数阶:O(nlogn)

快排、堆排序、归并排序的平均时间复杂度就是O(nlogn)。

平方阶:O(n^2)

for(int i = 0; i < n; i++){
    for(int j = 0; j < n; j++){
    
    }
}

插入、选择、冒泡都是O(n^2)的时间复杂度。

立方阶:O(n^3)

for(int i = 0; i < n; i++){
    for(int j = 0; j < n; j++){
        for(int k = 0; k < n; k++){
    
        }
    }
}

指数阶:O(2^n)

常见的求子集问题,就是指数阶复杂度。

阶乘阶:O(n!)

全排列问题

注意:在谈一个算法时间复杂度时,一般考虑的都是最坏时间复杂度

2.6.5 数组与链表

概念

数组

数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
链表

链表也是一种线性的数据结构,与数组不同的是,链表并不需要连续的内存空间,链表是通过指针的方式把每一个数据串联起来,对于单向链表来说,会记录一个后继指针next,而对于双向链表来说,除了后继指针next之外,还有一个前继指针pre。

随机访问

数组

我们都知道数组可以做到O(1)的随机访问,那原因就在于连续的内存空间和相同的数据类型。假设我们定义一个数组大小为5,并且存储的是int类型的数据,那么在内存中就会为其分配一块连续的空间,假设地址为:10~99(按照int类型占4个字节来计算),那么现在要访问数组中随便一个数据就可以通过这个公式来实现:arr[i] = 10 + i * 4

链表

链表中要想随机访问一个元素,就不能像数组一样做到O(1)的时间复杂度了,链表并不要求内存连续,所以没办法通过计算直接找到数据在内存中的位置,对于链表来说只能从头节点或者尾节点开始挨个遍历,因此其时间复杂度是O(n)的。

插入、删除

数组

正是因为内存连续性的要求,也导致了数组在插入和删除时相对低效,假设我们要在数组第一个位置插入一个元素,那么就会造成从第一个位置开始,之后的所有位置都需要往后挪一位,所以其时间复杂度为O(n),但如果只是往最后一个位置插入一个元素,那其时间复杂度还是O(1),或者插入元素后不必保证原始数组中的顺序不变,那我们也可以做赋值操作,先把待插入位置上的元素查找出来,并添加到数组最后,然后再用待插入的元素覆盖原位置上的元素即可,这样操作下来其时间复杂度也是O(1)的。

删除和插入一样的问题,如果删除数组末尾的元素,时间复杂度也是O(1)的,但如果删除的是数组中第一个位置的元素,那也需要把之后的每一个元素都往前挪一位,但实际上在某些场景中,删除也可以稍加改动,比如每次删除时并不真正的删除,只有先做一个标记位,待到真正空间不足时,再一次性删除,这样就减少了每次删除都需要移动数据的问题。

链表

这里有个误区,每当问到链表和数组的区别时,就有人会说链表的插入、删除快,数组的插入、删除慢,这种说法是不严谨的,链表只有在头节点或者尾节点插入、删除时可以实现O(1)的时间复杂度,如果是在某个指定的位置插入、删除时,就不一样了,链表必须从头或者尾开始挨个遍历,直到找到目标值,而这个查找的过程时间复杂度是O(n)的。

实际场景中需要注意的地方

  • 数组虽然简单,但对于内存有连续性的要求,如果一次性申请太大的数组空间,可能由于连续性空间不足,导致需要进行额外的内存碎片处理(甚至因为始终找不到一块连续的空间,而提示内存不足),而链表则没有这个问题。
  • 数组是一种固定大小的存储结构,一旦申请大小就固定了,如果大小不够,就需要重新申请更大的空间,然后自己把原来的数据拷贝过去,缩小也是同样的道理,但对于链表来说则没有这个问题,链表天然地支持动态扩容与缩容。
  • 链表中每个节点都需要额外的记录指向下一个节点的指针(双向链表还需要记录指向前一个节点的指针),所以会额外消耗这部分内存空间,而数组则没有这个方面的消耗。
  • 由于链表每个节点在内存中没有连续性的要求,所以如果频繁地对链表进行插入、删除则有可能造成内存碎片严重。
  • 由于数组内存连续性的特点,可以充分利用pagecache的预读性,实现更高效的访问。

2.7 异常

异常处理应该算是一种我们非常熟悉的话题了,Java中对于异常的处理也非常便捷、灵活,但往往越是简单的东西,越容易忽视它,恰巧异常也存在很多容易忽视的陷阱,一起来看看吧!

2.7.1 异常类型

Throwable

Throwable是所有异常的错误的父类,printStackTrace()方法就是由Throwable提供的。

Error

Error表示程序遇到了无法处理的问题,出现了严重的错误,常见的比如:OutOfMemoryError,StackOverflowError

Exception

程序本身可以处理的异常,Exception类本身又分为两类:运行时异常和编译时异常。

运行时异常

RuntimeException类及其子类产生的异常,编译时不会进行检查,只有在程序运行时才会产生,也可以通过try-catch来进行处理,但通常不需要我们这样做,因为运行时异常一般都是我们代码本身编写存在问题,应该在处理逻辑上进行修正。
常见的有:NullPointerException,ArrayIndexOutBoundException,ClassCastException

编译时异常

Exception下除了RuntimeException类型的其他异常都是编译时异常,这类异常在编译时就会进行检查,并强制要求对其进行处理,否则无法通过编译。
常见的有:ClassNotFoundException、IOException

2.7.2 异常使用的误区

忽视异常

绝大多数情况下都不应该像如下这样忽视异常的存在,因为这样会让你无法发现问题。

try{
    doSomething();
}catch(Exception e){
    // 什么也不做
}

当然也有例外

如果选择了忽略异常,那么最好在catch中通过注释的方式给出原因,并且变量名使用ignored

try {
              
} catch (ParseException ignored) {
}

标准化异常

统一语言、统一认知一直是我们强调的,让异常标准化也算其实现手段之一,得益于标准化的好处,当你看到如下这些异常时,会感到非常的熟悉:NullPointerException、IllegalArgumentException、IllegalStateException、ClassCastException、IllegalFormatConversionException、IndexOutOfBoundsException

如果没有这些标准化的异常分类,实际上所有的异常都可以归为IllegalStateException(非法状态)或者IllegalArgumentException(非法参数)。

比如:TreeMap中的Key不允许为null,HashTable中的value不允许为null

以上两个案例,实际上都可以按照IllegalArgumentException(非法参数)来处理,但是作者并没有这样做,IndexOutOfBoundsException异常也一样,并没有用IllegalArgumentException来替代。

常见的一些标准异常:

IllegalArgumentException
IndexOutOfBoundsException
NullPointerException
ClassCastException
IllegalFormatConversionException
UnsupportedOperationException

正确的使用异常

一种基于异常的循环控制,这种做法的原因是有人认为JVM底层就是这样终止的。

List<User> userList = new ArrayList<>();
userList.add(new User("a"));
userList.add(new User("b"));
userList.add(new User("c"));
userList.add(new User("d"));

try {
    int i = 0;
    while (true) {
        User user = userList.get(i++);
        System.out.println(user.getName());
    }
} catch (IndexOutOfBoundsException e) {
    // 什么也不做
}

比如,正常你应该会写成像下面这样,那JVM又是怎么判断数据边界的呢?

for (User user : userList) {
    System.out.println(user.getName());
}

为了省去每次的边界检查,所以采用异常捕获的方式,这明显是错误的,实际上测试对比后,后者比前者快很多,原因主要在于以下两点:

  • 写在try-catch中的代码,JVM一般不会对其进行优化。
  • 而数组的遍历,经过JVM优化后不会造成多余的边界检查。

基于上述这个案例,也告诫我们在做设计时,不要企图让你的调用者通过异常控制的方式来完成正常的流程。

再来看一个案例

Iterator<User> iterator = userList.iterator();
while(iterator.hasNext()){
    User user = iterator.next();
}

假如Iterator没有提供hasNext方法,那可能你只能通过try-catch的方式来解决了。

Iterator<User> iterator = userList.iterator();
try{
    while(true){
        User user = iterator.next();
    }
} catch(NoSuchElementException e){

}

让异常保持原子性

这条原则的含义是指,当调用某行代码产生异常时,应该使当前对象仍能保持在异常前的数据状态。

通常有下面几种方式:

让异常前置

举一个list集合移除元素的例子,其中rangeCheck方法中对当前集合的size做了检查,如果index >= size则抛出异常

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

其实通过这个简单的rangeCheck方法就能让异常保持原子性,因为它使得modCount在修改之前就已经抛出了异常,假设你没有提前做rangeCheck检查,那么你在调用E oldValue = elementData(index)这一行时,仍然会遇到IndexOutOfBoundsException异常,但modCount状态却已经被修改了,你不得不再去维护它的状态。

不可变对象

很多场景中不可变对象总是安全的,异常也不例外。

临时拷贝

如果你每次操作的都是新拷贝出来的对象,那么即使失败了,也并没有对原数据产生影响。
补偿
通过手动补偿的方式来保证失败后状态的正确性,就有点像如何解决分布式事务的问题,在遇到失败后,主动调用一段事先准备好的回滚逻辑,使数据回到失败前的状态。

2.7.3 其他建议

  • 处于事务中的流程,如果catch了异常,要注意事务的回滚。
  • 尽量避免在循环体中try-catch异常。
  • 不要用异常来控制流程
  • 使用try-with-resources替代try-catch-finally

2.8 事务

2.8.1 事务的传播

REQUIRED:支持当前事务,如果当前不存在则新开启一个事务(默认配置)
SUPPORTS:支持当前事务,如果当前不存在事务则以非事务方式执行
MANDATORY:支持当前事务,如果当前不存在事务则抛出异常
REQUIRES_NEW:创建一个新事务,如果当前已存在事务则挂起当前事务
NOT_SUPPORTED:以非事务方式执行,如果当前已存在事务则挂起当前事务
NEVER:以非事务方式执行,如果当前已存在事务则抛出异常
NESTED:如果当前存在事务,则在嵌套事务中执行,否则开启一个新事务

REQUIRED

当调用T1Service中的func方法时,除了更新t1表数据外,还会调用t2Service的func方法,更新t2表。

@Service
public class T1Service {

    @Resource
    private TestMapper testMapper;

    @Resource
    private T2Service t2Service;
    
    @Transactional
    public void func() {
        testMapper.updateT1();
        t2Service.func();
    }
}

@Service
public class T2Service {

    @Resource
    private TestMapper testMapper;

    @Transactional
    public void func() {
        testMapper.updateT2();
        int i = 1 / 0;
    }
}

@Transactional默认的传播方式就是REQUIRED,所以当方法执行到int i = 1 / 0时,会抛出异常,t1、t2表中的数据都不会被修改。

SUPPORTS

t2Service的func方法现在没有事务了,t2Service的func方法配置上@Transactional(propagation = Propagation.SUPPORTS),当执行int i = 1 / 0时,t1、t2两张表数据都不会回滚,但如果配置成@Transactional(propagation = Propagation.REQUIRED),则t2表数据会被回滚。

@Service
public class T1Service {

    @Resource
    private TestMapper testMapper;

    @Resource
    private T2Service t2Service;

    // @Transactional
    public void func() {
        testMapper.updateT1();
        t2Service.func();
    }
}

@Service
public class T2Service {

    @Resource
    private TestMapper testMapper;
    
    /**
     * 数据不会回滚,因为当前没有事务,SUPPORTS会以非事务方式执行
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    public void func() {
        testMapper.updateT2();
        int i = 1 / 0;
    }
}

MANDATORY

当t1Service没有事务时,把t2Service的func方法,配置为@Transactional(propagation = Propagation.MANDATORY)


// t1Service
public void func() {
    testMapper.updateT1();
    t2Service.func();
}

// t2Service
@Transactional(propagation = Propagation.MANDATORY)
public void func() {
    testMapper.updateT2();
    int i = 1 / 0;
}

抛出异常

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:362) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:595) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:382) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.14.jar:5.3.14]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.14.jar:5.3.14]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.14.jar:5.3.14]

REQUIRES_NEW

毫无疑问,t2的数据不会被更新,当没有事务时,REQUIRES_NEW会自己创建一个事务

// t1Service
public void func() {
    testMapper.updateT1();
    t2Service.func();
}

// t2Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void func() {
    testMapper.updateT2();
    int i = 1 / 0;
}

与REQUIRED有什么区别呢?
现在把抛出异常的地方放到t1Service中

// t1Service
@Transactional
public void func() {
    testMapper.updateT1();
    t2Service.func();
    int i = 1 / 0;
}

// t2Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void func() {
    testMapper.updateT2();
}

此时执行后,t2的数据不会回滚,t1的数据会回滚,因为t2和t1不是一个事务。

NOT_SUPPORTED

NOT_SUPPORTED的效果就是无论异常是在t1Service还是t2Service都不会回滚t2的数据。

// t1Service
@Transactional
public void func() {
    testMapper.updateT1();
    t2Service.func();
    int i = 1 / 0;
}

// t2Service
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void func() {
    testMapper.updateT2();
    int i = 1 / 0;
}

NEVER

很明显,如果存在事务,直接抛出异常

// t1Service
@Transactional
public void func() {
    testMapper.updateT1();
    t2Service.func();
}

// t2Service
@Transactional(propagation = Propagation.NEVER)
public void func() {
    testMapper.updateT2();
}
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.handleExistingTransaction(AbstractPlatformTransactionManager.java:413) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:352) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:595) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:382) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.14.jar:5.3.14]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.14.jar:5.3.14]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.14.jar:5.3.14]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.14.jar:5.3.14]

如果把t1Service中的事务去掉,则没问题,但t2Service抛出异常后,也不会回滚

// t1Service
public void func() {
    testMapper.updateT1();
    t2Service.func();
}

// t2Service
@Transactional(propagation = Propagation.NEVER)
public void func() {
    testMapper.updateT2();
    int i = 1 / 0;
}

NESTED

NESTED应该是几种事务传播方式中最难理解的,如果不注意,NESTED和REQUIRED功能看起来则差不多,都可以理解为有事务则加入,没有则新启一个,但实际上NESTED比REQUIRED要更加灵活。
先来看第一个案例,在t1Service中调用t2Service时,对异常进行了捕获,并且也没有抛出。

// t1Service
@Transactional
public void func() {
    testMapper.updateT1();
    try {
        t2Service.func();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// t2Service
@Transactional(propagation = Propagation.REQUIRED)
public void func() {
    testMapper.updateT2();
    int i = 1 / 0;
}

t2Service配置为REQUIRED时,t1、t2都进行了回滚,因为是同一个事务,但如果t2Service配置为NESTED就不一样了,此时t1则不会回滚。

// t1Service
@Transactional
public void func() {
    testMapper.updateT1();
    try {
        t2Service.func();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// t2Service
@Transactional(propagation = Propagation.NESTED)
public void func() {
    testMapper.updateT2();
    int i = 1 / 0;
}

特别说明

NESTED和REQUIRES_NEW的区别

现在有人可能觉得NESTED和REQUIRES_NEW有点相似,但实际上要注意NESTED和REQUIRES_NEW是很大的区别的。

现在我们分别给t1Service和t2Service加上一个TransactionSynchronizationManager.getCurrentTransactionName()输出看看效果。

// t1Service
@Transactional
public void func() {
    testMapper.updateT1();
    System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
    try {
        t2Service.func();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
// t2Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void func() {
    testMapper.updateT2();
    System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
    int i = 1 / 0;
}
输出结果
com.demo.transaction.service.T1Service.func
com.demo.transaction.service.T2Service.func

把REQUIRES_NEW替换为NESTED,可以看出使用NESTED后,实际上还是同一个事务。

com.demo.transaction.service.T1Service.func
com.demo.transaction.service.T1Service.func

NESTED实现方式

这就是NESTED不同之处,两个方法同一个事务,居然没有一起回滚,这就叫嵌套事务,子事务回滚不会影响到主事务,实际上利用的是savepoint功能,就好像下面这样

-- 主事务
savepoint;
-- 执行主事务代码

-- 子事务
savepoint;

-- 执行子事务代码

-- 子事务提交
commit;

-- 执行主事务代码

-- 主事务提交
commit;

所以,如果是在主事务中抛出异常,那么子事务也会被回滚,就像下面这样。

// t1Service
@Transactional
public void func() {
    testMapper.updateT1();
    t2Service.func();
    int i = 1 / 0;
}
// t2Service
@Transactional(propagation = Propagation.NESTED)
public void func() {
    testMapper.updateT2();
}

2.8.2 事务应用

通过Spring来开启事务管理非常简单,默认支持两种方式,一种为编程式事务、一种为声明式事务,大多数人对声明式事务都比较熟悉,我们就先从它开始说起。
声明式事务
开启声明式事务非常简单,直接通过@Transactional注解即可

@Transactional
public void func() {
    
}

除了常规的支持自定义事务的隔离级别、传播属性之外,还可以设置事务的超时时间,回滚的异常类型。

public @interface Transactional {

    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    String timeoutString() default "";

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};

}

编程式事务

声明式事务的最大问题就在于,粒度控制问题,声明式事务最细的粒度也是方法级别的,这很容易导致长事务问题的产生,所以我们一般使用编程式事务替代。
Spring提供的TransactionTemplate、PlatformTransactionManager都支持编程式事务的实现,TransactionTemplate是在原始的事务管理类上又封装了一次,调用其核心方法execute实现整个事务的管理。

@Override
@Nullable
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
    Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
    if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
    return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
    }
    else {
    TransactionStatus status = this.transactionManager.getTransaction(this);
    T result;
    try {
    result = action.doInTransaction(status);
    }
    catch (RuntimeException | Error ex) {
    // Transactional code threw application exception -> rollback
    rollbackOnException(status, ex);
    throw ex;
    }
    catch (Throwable ex) {
    // Transactional code threw unexpected exception -> rollback
    rollbackOnException(status, ex);
    throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
    }
    this.transactionManager.commit(status);
    return result;
    }
}

PlatformTransactionManager则更灵活一点,就定义了三个关键方法,一看就明白了

public interface PlatformTransactionManager extends TransactionManager {

    
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
    throws TransactionException;

    
    void commit(TransactionStatus status) throws TransactionException;

    
    void rollback(TransactionStatus status) throws TransactionException;

}

TransactionTemplate演示

execute中入参为TransactionCallback,这是一个函数式接口,只定义了一个方法doInTransaction
其可以传入TransactionCallbackWithoutResult不带返回参数的。

@Resource
private TransactionTemplate transactionTemplate;

public void func() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            testMapper.updateT1();
            t2Service.func();
            int i = 1 / 0;
        }
    });
}

或者直接传入TransactionCallback带返回参数的也可以。

PlatformTransactionManager演示

@Resource
private PlatformTransactionManager platformTransactionManager;

public void func() {
    TransactionStatus status = platformTransactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        testMapper.updateT1();
        t2Service.func();
        int i = 1 / 0;
        platformTransactionManager.commit(status);
    } catch (Exception e) {
        e.printStackTrace();
        platformTransactionManager.rollback(status);
    }
}

事务使用的注意事项

滥用@Transactional

不过,也因为它的简单灵活,而经常导致被滥用的情况发生。
千万不要直接把注解加在Service上,这会导致整个Service中的方法,只要调用到数据库,就都会被进行事务管理,从而影响数据库和Web服务的QPS。

@Service
@Transactional // 不要加在Service上
public class DemoService {

    
}
长事务、过早起开事务

简单来说,就是在整个方法的生命周期内,真正需要事务管理的方法可能只占用了200毫秒,而其他业务流程占用了2秒,但是由于事务是对整个方法生效,从而导致一个数据库连接被占用2秒多。

@Transactional
public void func() {
    // 两个select花费了2秒
    select1();
    select2();
    // 两个save只花费了200毫秒
    save1();
    save2();
}

解决方式也很简单,把长事务拆分为短事务即可。

public void func() {
    select1();
    select2();
    manager.save();
}

@Transactional
public void save() {
    save1();
    save2();
}
锁的粒度

要想开启事务,就要先持有锁,因此锁的范围就很重要,InnoDB之所以能够取代MyISAM,不单单只是因为InnoDB支持事务,更重要的是因为它还支持行级锁,这在高并发的业务场景中,是非常关键的。
所以,我们平时在写代码时,一定要注意避免表级锁的产生。

数据库死锁

看到此类的异常,不用多想,基本上就是并发事务导致的,一个事务还未结束,另一个事务想再获取锁时就会遇到这个问题。

Deadlock found when trying to get lock; try restarting transaction

解决的方式就前面提到的两点:

  1. 避免长事务。
  2. 缩小锁的粒度。
    当然,如果并发真的高到,单行更新时也存在冲突,那就只能通过锁,或者改批量同步的方式了。

2.8.3 事务的失效场景

异常未抛出

被捕获的异常一定要抛出,否则是不会回滚的。

// t1Service
@Transactional
public void func() {
    try {
        testMapper.updateT1();
        t2Service.func();
        int i = 1 / 0;
    } catch (Exception e) {
        // 异常捕获了,未抛出,导致异常事务不会回滚。
        e.printStackTrace();
    }
}

// t2Service
@Transactional
public void func() {
    testMapper.updateT2();
}

异常与rollback不匹配

@Transactional默认情况下,只会回滚RuntimeException和Error及其子类的异常,如果是受检异常或者其他业务类异常是不会回滚事务的。

@Transactional
public void func() throws Exception {
    try {
        testMapper.updateT1();
        t2Service.func();
        int i = 1 / 0;
    } catch (Exception e) {
        // 默认情况下非运行时异常不会回滚
        throw new Exception();
    }
}

修改方式也很简单,@Transactional支持通过rollbackFor指定回滚异常类型

// 改成rollbackFor = Exception.class即可
@Transactional(rollbackFor = Exception.class)
public void func() throws Exception {
    try {
        testMapper.updateT1();
        t2Service.func();
        int i = 1 / 0;
    } catch (Exception e) {
        throw new Exception();
    }
}

方法内部直接调用

func2方法是由func调用,虽然func2方法上加了@Transactional注解,但事务不会生效,testMapper.updateT2()执行的方法并不会回滚

public void func() {
    testMapper.updateT1();
    func2();
}
@Transactional
public void func2() {
    testMapper.updateT2();
    int i = 1 / 0;
}

修改方式也很简单,通过注入的方式调用即可

@Service
public class T1Service {

    @Resource
    private TestMapper testMapper;
   
   // 注入T1Service对象
    @Resource
    private T1Service t1Service;

    public void func() {
        testMapper.updateT1();
        // 通过注入的方式调用自身的方法
        t1Service.func2();
    }

    @Transactional
    public void func2() {
        testMapper.updateT2();
        int i = 1 / 0;
    }
}

小插曲,SpringBoot 2.6.0版本开发,默认禁止循环依赖,所以如果你使用的版本是2.6.0之后的,那么启动会遇到如下报错
As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
修改方式:在配置文件中把允许循环依赖打开即可。
spring.main.allow-circular-references=true

当然,你也可以直接使用AopContext的方式

public void func() {
    testMapper.updateT1();
    T1Service t1Service = (T1Service) AopContext.currentProxy();
    t1Service.func2();
}
@Transactional
public void func2() {
    testMapper.updateT2();
    int i = 1 / 0;
}

在另一个线程中使用事务

Spring事务管理的方式就是通过ThreadLocal把数据库连接与当前线程绑定,如果新开启一个线程自然就不是一个数据库连接了,自然也就不是一个事务。

t2Service.func()方法操作的数据并不会被回滚

@Transactional
public void func() {
    testMapper.updateT1();
    new Thread(() -> t2Service.func()).start();
    int i = 1 / 0;
}

注解作用到private级别的方法上

当你写成如下这样时,IDEA直接会给出提示Methods annotated with ‘@Transactional’ must be overridable 原因很简单,private修饰的方式,spring无法为其生成代理。

public void func() {
    t1Service.func2();
}
@Transactional
private void func2() {
    testMapper.updateT1();
    int i = 1 / 0;
}

final类型的方法

这个与private道理是一样的,都是影响了Spring生成代理对象,同样IDEA也会有相关提示。

数据库存储引擎不支持事务

注意,如果你使用的是MySQL数据库,那么常用的存储引擎中只有InnoDB才支持事务,像MyISAM是不支持事务的,其他存储引擎都是针对特定场景下使用的,一般也不会用到,不做讨论。

事务的传播类型

前面已经对事务的传播类型做过介绍了,有的传播类型会以非事务方式执行,有的传播则会新开启一个事务,这些都需要额外注意。

REQUIRED:支持当前事务,如果当前不存在则新开启一个事务(默认配置)
SUPPORTS:支持当前事务,如果当前不存在事务则以非事务方式执行
MANDATORY:支持当前事务,如果当前不存在事务则抛出异常
REQUIRES_NEW:创建一个新事务,如果当前已存在事务则挂起当前事务
NOT_SUPPORTED:以非事务方式执行,如果当前已存在事务则挂起当前事务
NEVER:以非事务方式执行,如果当前已存在事务则抛出异常
NESTED:如果当前存在事务,则在嵌套事务中执行,否则开启一个新事务

2.9 注释

程序员最讨厌的四件事:写注释、写文档、别人不写注释、别人不写文档,虽然是个段子,但也侧面反映出写注释的问题确实存在。

2.9.1 到底该不该写注释

【代码整洁之道】一书中有个理念就是,注释是为了弥补代码表达能力不足的一种不得已的做法。如果代码能表达清楚,那就没必要写注释,作者甚至认为只要你写了注释,就说明你的代码写得不够好,我觉得作者应该是为了让你能够更加努力地写出好的代码,而不是试图用注释来弥补你的烂代码,这本身和写注释是不冲突的,很明显,如果代码已经非常糟糕了,那么请直接重构它,而不是企图用注释去弥补。

2.9.2 言简意赅

注释一定是对代码逻辑的概括,不需要长篇大论地逐行说明,表明你的意图即可。

2.9.3 准确性

我想你一定遇到过,注释的描述与实际代码执行的逻辑不相符的情况,这真的是非常让你头疼,之所以会出现这种情况,大多数是因为由于需求的迭代,代码逻辑修改后,注释没跟着一起修改,所以,修改代码逻辑时,请一定要注意注释是否也需要一起修改。

你可能感兴趣的:(java,代码规范,后端)