关于如何写好代码的一些建议与方法(上)
关于如何写好代码的一些建议与方法(中)
关于如何写好代码的一些建议与方法(下)
数据结构一般被分为:逻辑结构与存储结构(内存)
逻辑结构:线性结构、树形结构、图形结构
存储结构:顺序存储、链式存储
线性结构包含:数组、链表、栈、队列,其特点是由一系列数据类型相同的数据元素组成的有序集合。
线性结构的优点是操作简单、速度快,随机访问能力强,但缺点是一旦数据量增加后,要么访问的速度变慢,要么插入或者删除的速度变慢,因此线性结构通常用于处理简单的数据集合,例如Java语言中的List、LinkedList、Queue
等容器集合。
树这种数据结构就比线性结构要复杂得多了,树一般由多个节点按照层次结构连接而成,常见的树有二叉树、平衡树、红黑树、B树和B+树等,树形结构通常用来解决大数据量之下的数据检索以及操作的问题。
图形结构和前两种结构相比,又是一种更复杂的数据结构了,它是由节点和边组成的,节点表示图形中的一个点,边表示两个节点之间的关系。在图形数据结构中,每个节点都有一个唯一的标识符,称为节点ID,而边则用于描述节点之间的关系,如边的权重和边的方向等。
图形数据结构通常用于表示具有网络或连通性的实体或概念,例如社交网络中的好友关系、城市地图等,可以说,凡是多对多的关系,都可以看作是图形结构。
顺序存储是将数据按照一定的顺序排列存储在存储器中。这种方式的优点是访问速度快,适合存储结构简单的数据。但是,当需要插入或删除数据时,需要移动后面的数据,时间复杂度较高。
数组、栈、队列、树(树这种结构既可以用顺序存储也可以用链式存储,比如最为典型的就是堆这样的树形结构,它就是用数组来存储的)都可以用顺序存储来完成。
链式存储是将数据存储在存储器中的不同位置,每个位置上存储的是一个指针,指向存储在其他位置的数据。这种方式的优点是插入和删除数据的效率高,时间复杂度为O(1)。但是,访问某个数据需要从头开始遍历,时间复杂度较高。
链表、树、图都可以用链式存储来完成。
结合这些数据结构的特点,希望你能在增删改查的场景中进行正确的选择。
来一道小学生难道的数学题:计算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),如果要加到一万,一千万,一亿,节省的计算量相当可观。
复杂度一般分为:时间复杂度、空间复杂度。
大多数场景中更注重的是时间复杂度,根据其执行效率,一般又可分为如下几种:
常量阶: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!)
全排列问题
注意:在谈一个算法时间复杂度时,一般考虑的都是最坏时间复杂度
数组
数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
链表
链表也是一种线性的数据结构,与数组不同的是,链表并不需要连续的内存空间,链表是通过指针的方式把每一个数据串联起来,对于单向链表来说,会记录一个后继指针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)的。
异常处理应该算是一种我们非常熟悉的话题了,Java中对于异常的处理也非常便捷、灵活,但往往越是简单的东西,越容易忽视它,恰巧异常也存在很多容易忽视的陷阱,一起来看看吧!
Throwable是所有异常的错误的父类,printStackTrace()
方法就是由Throwable提供的。
Error表示程序遇到了无法处理的问题,出现了严重的错误,常见的比如:OutOfMemoryError,StackOverflowError
程序本身可以处理的异常,Exception类本身又分为两类:运行时异常和编译时异常。
运行时异常
RuntimeException
类及其子类产生的异常,编译时不会进行检查,只有在程序运行时才会产生,也可以通过try-catch
来进行处理,但通常不需要我们这样做,因为运行时异常一般都是我们代码本身编写存在问题,应该在处理逻辑上进行修正。
常见的有:NullPointerException,ArrayIndexOutBoundException,ClassCastException
编译时异常
Exception
下除了RuntimeException
类型的其他异常都是编译时异常,这类异常在编译时就会进行检查,并强制要求对其进行处理,否则无法通过编译。
常见的有:ClassNotFoundException、IOException
绝大多数情况下都不应该像如下这样忽视异常的存在,因为这样会让你无法发现问题。
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());
}
为了省去每次的边界检查,所以采用异常捕获的方式,这明显是错误的,实际上测试对比后,后者比前者快很多,原因主要在于以下两点:
基于上述这个案例,也告诫我们在做设计时,不要企图让你的调用者通过异常控制的方式来完成正常的流程。
再来看一个案例
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
状态却已经被修改了,你不得不再去维护它的状态。
不可变对象
很多场景中不可变对象总是安全的,异常也不例外。
临时拷贝
如果你每次操作的都是新拷贝出来的对象,那么即使失败了,也并没有对原数据产生影响。
补偿
通过手动补偿的方式来保证失败后状态的正确性,就有点像如何解决分布式事务的问题,在遇到失败后,主动调用一段事先准备好的回滚逻辑,使数据回到失败前的状态。
catch
了异常,要注意事务的回滚。try-catch
异常。try-with-resources
替代try-catch-finally
REQUIRED:支持当前事务,如果当前不存在则新开启一个事务(默认配置)
SUPPORTS:支持当前事务,如果当前不存在事务则以非事务方式执行
MANDATORY:支持当前事务,如果当前不存在事务则抛出异常
REQUIRES_NEW:创建一个新事务,如果当前已存在事务则挂起当前事务
NOT_SUPPORTED:以非事务方式执行,如果当前已存在事务则挂起当前事务
NEVER:以非事务方式执行,如果当前已存在事务则抛出异常
NESTED:如果当前存在事务,则在嵌套事务中执行,否则开启一个新事务
当调用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表中的数据都不会被修改。
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;
}
}
当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]
毫无疑问,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的效果就是无论异常是在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;
}
很明显,如果存在事务,直接抛出异常
// 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和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();
}
通过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);
}
}
不过,也因为它的简单灵活,而经常导致被滥用的情况发生。
千万不要直接把注解加在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
解决的方式就前面提到的两点:
- 避免长事务。
- 缩小锁的粒度。
当然,如果并发真的高到,单行更新时也存在冲突,那就只能通过锁,或者改批量同步的方式了。
被捕获的异常一定要抛出,否则是不会回滚的。
// 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();
}
@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;
}
当你写成如下这样时,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;
}
这个与private
道理是一样的,都是影响了Spring生成代理对象,同样IDEA也会有相关提示。
注意,如果你使用的是MySQL数据库,那么常用的存储引擎中只有InnoDB才支持事务,像MyISAM是不支持事务的,其他存储引擎都是针对特定场景下使用的,一般也不会用到,不做讨论。
前面已经对事务的传播类型做过介绍了,有的传播类型会以非事务方式执行,有的传播则会新开启一个事务,这些都需要额外注意。
REQUIRED:支持当前事务,如果当前不存在则新开启一个事务(默认配置)
SUPPORTS:支持当前事务,如果当前不存在事务则以非事务方式执行
MANDATORY:支持当前事务,如果当前不存在事务则抛出异常
REQUIRES_NEW:创建一个新事务,如果当前已存在事务则挂起当前事务
NOT_SUPPORTED:以非事务方式执行,如果当前已存在事务则挂起当前事务
NEVER:以非事务方式执行,如果当前已存在事务则抛出异常
NESTED:如果当前存在事务,则在嵌套事务中执行,否则开启一个新事务
程序员最讨厌的四件事:写注释、写文档、别人不写注释、别人不写文档,虽然是个段子,但也侧面反映出写注释的问题确实存在。
在【代码整洁之道】
一书中有个理念就是,注释是为了弥补代码表达能力不足的一种不得已的做法。如果代码能表达清楚,那就没必要写注释,作者甚至认为只要你写了注释,就说明你的代码写得不够好,我觉得作者应该是为了让你能够更加努力地写出好的代码,而不是试图用注释来弥补你的烂代码,这本身和写注释是不冲突的,很明显,如果代码已经非常糟糕了,那么请直接重构它,而不是企图用注释去弥补。
注释一定是对代码逻辑的概括,不需要长篇大论地逐行说明,表明你的意图即可。
我想你一定遇到过,注释的描述与实际代码执行的逻辑不相符的情况,这真的是非常让你头疼,之所以会出现这种情况,大多数是因为由于需求的迭代,代码逻辑修改后,注释没跟着一起修改,所以,修改代码逻辑时,请一定要注意注释是否也需要一起修改。