transacción de primavera lea solo este artículo

1. Descripción general del negocio

Una transacción es lógicamente un conjunto de operaciones que se ejecutan o no se ejecutan. Principalmente para bases de datos, como MySQL.

1.1 Transacciones de la base de datos MySQL

4 características importantes de la base de datos MYSQL ACID :

característica describir
Atomicidad Todas las operaciones en una transacción se completan o no se completan, y no terminarán en un enlace determinado en el medio. Si ocurre un error durante la ejecución de la transacción, se revertirá (Rollback) al estado anterior al inicio de la transacción, como si la transacción nunca se hubiera ejecutado.
Consistencia La integridad de la base de datos no se ve comprometida antes de que comience la transacción y después de que finalice.
Aislamiento de transacciones (Aislamiento) La base de datos permite que múltiples transacciones simultáneas lean, escriban y modifiquen sus datos al mismo tiempo. El aislamiento puede evitar la inconsistencia de datos causada por la ejecución cruzada cuando se ejecutan múltiples transacciones simultáneamente.
Durabilidad Una vez que finaliza el procesamiento de la transacción, la modificación de los datos es permanente, incluso si el sistema falla, no se perderá

Nivel de aislamiento de transacciones de la base de datos MYSQL:

nivel de aislamiento describir
Lectura no confirmada (Lectura no confirmada) El nivel de aislamiento más bajo, que permite "lecturas sucias" (dirty reads), las transacciones pueden ver las modificaciones "todavía no confirmadas" de otras transacciones. Si otra transacción retrocede, los datos leídos por la transacción actual son datos sucios
Comprometerse a leer (leer comprometido) Una transacción puede encontrar el problema de lectura no repetible (Lectura no repetible). La lectura no repetible se refiere a leer los mismos datos varias veces dentro de una transacción. Si ocurre que otra transacción modifica los datos antes de que termine la transacción, entonces, en la primera transacción, los datos leídos dos veces pueden no ser consistentes.
Lectura repetible (lectura repetible) Una transacción puede encontrar el problema de lectura fantasma (Phantom Read). La lectura fantasma significa que en una transacción, la primera vez que se consulta un registro, se encuentra que no existe ningún registro, pero al intentar actualizar ese registro que no existe, puede tener éxito, y cuando se vuelve a leer el mismo registro , es mágico apareció
Serializable El nivel de aislamiento más estricto, todas las transacciones se ejecutan secuencialmente, por lo tanto, no se producirán lecturas sucias, lecturas no repetibles y lecturas fantasma. Aunque las transacciones bajo el nivel de aislamiento serializable tienen la mayor seguridad, sin embargo, dado que las transacciones se ejecutan en serie, la eficiencia se reducirá considerablemente y el rendimiento de la aplicación se reducirá drásticamente. Si no hay una situación particularmente importante, generalmente no se usa el nivel de aislamiento Serializable

1.2 compatibilidad con transacciones de primavera:

Spring admite dos tipos de transacciones: transacciones programáticas y transacciones declarativas.

/**
 * 模拟转账
 */
@Transactional
public void handle() {
    
    
 // 转账
 transfer(double money);
 // 减自己的钱
  Reduce(double money);
}

1.2.1 Transacciones programáticas:

Las transacciones programáticas se refieren a la incorporación de códigos de gestión de transacciones en códigos comerciales para controlar la confirmación y reversión de transacciones.
Método 1: use TransactionTemplate para administrar transacciones

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
    
    

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    
    
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
    
    

                try {
    
    

                    // ....  业务代码
                } catch (Exception e){
    
    
                    //回滚
                    transactionStatus.setRollbackOnly();
                }

            }
        });
}


Método 2: use TransactionManager para administrar transacciones

@Autowired
private PlatformTransactionManager transactionManager;

public void testTransaction() {
    
    

  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
    
    
               // ....  业务代码
              transactionManager.commit(status);
          } catch (Exception e) {
    
    
              transactionManager.rollback(status);
          }
}

Nota: para la gestión programática de transacciones, Spring prefiere usar TransactionTemplate.
En las transacciones programáticas, se debe incluir un código de gestión de transacciones adicional en cada operación comercial, lo que hace que el código parezca muy inflado, pero es muy útil para comprender el modelo de gestión de transacciones de Spring.

1.2.2 Transacción declarativa

Las transacciones declarativas separan el código de gestión de transacciones de los métodos comerciales e implementan la gestión de transacciones de manera declarativa Para los desarrolladores, las transacciones declarativas son obviamente más fáciles y mejores de usar que las transacciones programáticas.
Por supuesto, para lograr la separación de la gestión de transacciones y el código comercial, debe usar AOP en Spring. Su esencia es interceptar el método antes y después, y luego crear o unir una transacción antes de que comience el método de destino y ejecutar el método de destino A continuación, envíe o retroceda de acuerdo con la situación de ejecución.

Aunque las transacciones declarativas son mejores que las transacciones programáticas, también tienen deficiencias. La granularidad de la gestión de transacciones declarativas se encuentra en el nivel de método, mientras que las transacciones programáticas pueden ser precisas en el nivel de bloque de código.

Modelo de gestión de transacciones:
Spring abstrae el núcleo de la gestión de transacciones en un administrador de transacciones (TransactionManager), y su código fuente tiene solo una definición de interfaz simple, que pertenece a una interfaz de marcador:

public interface TransactionManager {
    
    

}

Esta interfaz tiene dos subinterfaces, a saber, la interfaz de transacción programática ReactiveTransactionManager y la interfaz de transacción declarativa PlatformTransactionManager. Centrémonos en PlatformTransactionManager, que define 3 métodos de interfaz:

interface PlatformTransactionManager extends TransactionManager{
    
    
    // 根据事务定义获取事务状态
    TransactionStatus getTransaction(TransactionDefinition definition)
            throws TransactionException;

    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;

    // 事务回滚
    void rollback(TransactionStatus status) throws TransactionException;
}

A través de la interfaz PlatformTransactionManager, Spring proporciona los administradores de transacciones correspondientes para varias plataformas, como JDBC (DataSourceTransactionManager), Hibernate (HibernateTransactionManager), JPA (JpaTransactionManager), etc., pero la implementación específica es asunto de cada plataforma.
El parámetro TransactionDefinition corresponde a la anotación @Transactional. Por ejemplo, los atributos definidos en la anotación @Transactional, como el comportamiento de propagación de la transacción, el nivel de aislamiento, el tiempo de espera de la transacción y si la transacción es de solo lectura, se pueden encontrar en TransactionDefinition.
El tipo de retorno TransactionStatus se usa principalmente para almacenar algunos estados y datos de la transacción actual, como recursos de transacción (conexión), estado de reversión, etc.
La definición de transacción es la siguiente:

public interface TransactionDefinition {
    
    

 // 事务的传播行为
 default int getPropagationBehavior() {
    
    
  return PROPAGATION_REQUIRED;
 }

 // 事务的隔离级别
 default int getIsolationLevel() {
    
    
  return ISOLATION_DEFAULT;
 }

  // 事务超时时间
  default int getTimeout() {
    
    
  return TIMEOUT_DEFAULT;
 }

  // 事务是否只读
  default boolean isReadOnly() {
    
    
  return false;
 }
}

Las anotaciones transaccionales son las siguientes:

@Target({
    
    ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    
    

 Propagation propagation() default Propagation.REQUIRED;
 Isolation isolation() default Isolation.DEFAULT;
  int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
  boolean readOnly() default false;

}

La propagación en la anotación @Transactional corresponde a getPropagationBehavior en TransactionDefinition y el valor predeterminado es Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED).
El aislamiento en la anotación @Transactional corresponde a getIsolationLevel en TransactionDefinition y el valor predeterminado es DEFAULT(TransactionDefinition.ISOLATION_DEFAULT).
El tiempo de espera en la anotación @Transactional corresponde a getTimeout en TransactionDefinition y el valor predeterminado es TransactionDefinition.TIMEOUT_DEFAULT.
readOnly en la anotación @Transactional corresponde a isReadOnly en TransactionDefinition, y el valor predeterminado es falso.
Hablando de eso, expliquemos en detalle el comportamiento de propagación de las transacciones de Spring, el nivel de aislamiento de las transacciones, el período de tiempo de espera de las transacciones, los atributos de solo lectura de las transacciones y las reglas de reversión de las transacciones.

Hablando de eso, expliquemos en detalle el comportamiento de propagación de las transacciones de Spring, el nivel de aislamiento de las transacciones, el período de tiempo de espera de las transacciones, los atributos de solo lectura de las transacciones y las reglas de reversión de las transacciones.

1.2.3 Comportamiento de propagación de transacciones:

Cuando un método de transacción es llamado por otro método de transacción, es necesario especificar cómo se debe propagar la transacción. Por ejemplo, el método puede continuar ejecutándose en la transacción actual, o puede iniciar una nueva transacción y ejecutarla por su cuenta. transacción.
El comportamiento de propagación de las transacciones declarativas se puede definir a través del atributo de propagación en la anotación @Transactional, por ejemplo:

@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
    
    
}

TransactionDefinition define un total de 7 comportamientos de propagación de transacciones, entre los cuales PROPAGATION_REQUIRED y PROPAGATION_REQUIRES_NEW son los más utilizados.

1. PROPAGATION_REQUIRED
Este es también el comportamiento predeterminado de propagación de transacciones de @Transactional, lo que significa que si existe una transacción actualmente, únase a la transacción, si no hay una transacción actual, cree una nueva transacción. Más precisamente, significa:
si el método externo no abre una transacción, el método interno modificado por Propagation.REQUIRED abrirá su propia transacción, y las transacciones abiertas son independientes entre sí y no interfieren entre sí.
Si el método externo inicia una transacción y es Propagation.REQUIRED, todos los métodos internos y los métodos externos modificados por Propagation.REQUIRED pertenecen a la misma transacción. Mientras se revierte un método, se debe revertir toda la transacción.

Es decir, si se anotan tanto el método a como el método b, en el modo de propagación predeterminado, el método a llama al método b internamente y las transacciones de los dos métodos se fusionarán en una transacción.

2. PROPAGATION_REQUIRES_NEW
crea una nueva transacción, si hay una transacción actual, suspende la transacción actual. Es decir, independientemente de si el método externo abre la transacción, el método interno modificado por Propagation.REQUIRES_NEW abrirá su propia transacción, y la transacción abierta y la transacción externa son independientes entre sí y no interfieren entre sí.

Cuando el método a en la clase A usa el modo Propagation.REQUIRED predeterminado, el método b en la clase B adopta el modo Propagation.REQUIRES_NEW y luego llama al método b en el método a para operar la base de datos. una excepción, el método b no No hay reversión, porque Propagation.REQUIRES_NEW suspenderá la transacción del método a. El resumen es que a no afecta a b, y b afecta a

3. PROPAGATION_NESTED
Si hay una transacción actual, se ejecutará en la transacción actual, de lo contrario, realizará una operación similar a PROPAGATION_REQUIRED.

Cuando el método a en la clase A usa el modo Propagation.REQUIRED predeterminado, el método b en la clase B adopta el modo Propagation.NESTED y luego llama al método b en el método a para operar la base de datos. una excepción, el método a No es una reversión, el resumen es que b no afecta a a, y a afecta a b.

4. PROPAGATION_SUPPORTS
Si hay una transacción actual, únase a la transacción, si no hay ninguna transacción actual, continúe ejecutándose de manera no transaccional.

5. PROPAGATION_NOT_SUPPORTED
se ejecuta en un modo no transaccional y, si hay una transacción actual, suspende la transacción actual.

6. PROPAGATION_MANDATORY
Si hay una transacción actual, únete a la transacción, si no hay una transacción actual, lanza una excepción.

7. PROPAGATION_NEVER
se ejecuta de manera no transaccional y lanza una excepción si hay una transacción actual.

1.2.4 Nivel de aislamiento de transacciones

Ya hemos entendido el nivel de aislamiento de transacciones de la base de datos antes, y es mucho más fácil entender el nivel de aislamiento de transacciones de Spring.
En TransactionDefinition se definen un total de 5 niveles de aislamiento de transacciones:

nivel de aislamiento describir
ISOLATION_DEFAULT Usando el nivel de aislamiento predeterminado de la base de datos, MySql usa REPEATABLE_READ de forma predeterminada, que es lectura repetible.
ISOLATION_READ_UNCOMMITTED Pueden ocurrir el nivel de aislamiento más bajo, lecturas sucias, lecturas fantasma o lecturas no repetibles
ISOLATION_READ_COMMITTED Permitir la lectura de datos enviados por transacciones simultáneas puede evitar lecturas sucias, pero aún pueden ocurrir lecturas fantasma y lecturas no repetibles.
ISOLATION_REPEATABLE_READ Los resultados de varias lecturas del mismo campo son consistentes, a menos que los datos sean modificados por su propia transacción, lo que puede evitar lecturas sucias y lecturas no repetibles, pero aún pueden ocurrir lecturas fantasma.
AISLAMIENTO_SERIALIZABLE El nivel de aislamiento más alto, aunque puede evitar lecturas sucias, lecturas fantasma y lecturas no repetibles, afectará seriamente el rendimiento del programa.

En circunstancias normales, podemos usar el nivel de aislamiento predeterminado ISOLATION_DEFAULT, que se deja a la decisión de la base de datos.

1.2.5 Tiempo de espera de transacción

El tiempo de espera de la transacción **tiempo de espera** se refiere al tiempo máximo que se permite ejecutar una transacción. Si no se completa dentro del período de tiempo de espera, se revertirá automáticamente.
Si el tiempo de ejecución de la transacción es extremadamente largo, porque la transacción implica el bloqueo de la base de datos, hará que la transacción de ejecución prolongada ocupe recursos de la base de datos.

1.2.6 El atributo de solo lectura de las transacciones

El atributo de solo lectura de la transacción es readOnly.Si una transacción solo realiza operaciones de lectura en la base de datos, entonces la base de datos puede aprovechar el atributo de solo lectura de la transacción y tomar medidas de optimización, que son adecuadas para múltiples operaciones de consulta de base de datos. .
¿Por qué es necesario que una operación de consulta habilite el soporte de transacciones?
Esto se debe a que MySql (innodb) habilita el modo de confirmación automática para cada conexión de manera predeterminada. En este modo, cada instrucción SQL enviada al servidor MySql se procesará en una transacción separada y la transacción se confirmará automáticamente después de la ejecución.
Luego, si agregamos la anotación @Transactional al método, todo el SQL en este método se colocará en una transacción. De lo contrario, cada instrucción SQL abrirá una transacción separada y los datos modificados por otras transacciones se leerán en tiempo real.
En algunos casos, cuando se ejecutan varias declaraciones de consulta a la vez y es necesario garantizar la coherencia de los datos, es necesario habilitar la compatibilidad con transacciones. De lo contrario, después de la última consulta SQL, otros usuarios modifican los datos, luego la siguiente consulta SQL puede aparecer en un estado inconsistente.

1.2.7 Estrategia de reversión de transacciones

**Estrategia de reversión rollbackFor**, utilizada para especificar el tipo de excepción que puede desencadenar la reversión de transacciones, se pueden especificar varios tipos de excepción. De manera predeterminada, la transacción solo se revierte cuando ocurre una excepción de tiempo de ejecución (Excepción de tiempo de ejecución), y Error no se revierte cuando ocurre una excepción verificada (excepción verificada, que debe capturarse activamente o lanzarse hacia arriba).

Si desea revertir un tipo de excepción específico, puede configurarlo así:

@Transactional(rollbackFor= MyException.class)

Política de no reversión de transacciones
** política de no reversión noRollbackFor **, utilizada para especificar los tipos de excepción que no activan la reversión de transacciones, y se pueden especificar varios tipos de excepciones.

2. 12 escenarios donde fallan las transacciones de primavera (anotación @Transactional)

En algunos escenarios comerciales, si una solicitud necesita escribir datos en varias tablas o ejecutar varios SQL al mismo tiempo. Para garantizar la atomicidad de las operaciones (ya sea exitosa o fallida al mismo tiempo) y evitar la inconsistencia de los datos, generalmente usamos transacciones de primavera.

2.1 La transacción no surte efecto [siete tipos]

2.1.1 Problemas de derechos de acceso (solo tendrán efecto los métodos públicos)

Como todos sabemos, existen cuatro tipos principales de permisos de acceso para Java: privado, predeterminado, protegido y público, y sus permisos aumentan de izquierda a derecha.

Sin embargo, si definimos permisos de acceso incorrectos para ciertos métodos de transacción durante el proceso de desarrollo, causará problemas con la función de transacción, por ejemplo:

@Service
public class UserService {
    
    
    
    @Transactional
    private void add(UserModel userModel) {
    
    
         saveData(userModel);
         updateData(userModel);
    }
}

Podemos ver que la autoridad de acceso del método add está definida como privada, lo que hará que la transacción falle, Spring requiere que el método proxy sea público.

Para decirlo sin rodeos, hay un juicio en el método computeTransactionAttribute de la clase AbstractFallbackTransactionAttributeSource: si el método de destino no es público, TransactionAttribute devuelve nulo, es decir, las transacciones no son compatibles.

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    
    
    // Don't allow no-public methods as required.可以看到, 这里不支持public类型的方法
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    
    
      return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
    
    
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
    
    
      return txAttr;
    }

    if (specificMethod != method) {
    
    
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
    
    
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
    
    
        return txAttr;
      }
    }
    return null;
  }

Es decir, si nuestro método de transacción personalizado (es decir, el método de destino) tiene derechos de acceso que no sean públicos, pero privados, predeterminados o protegidos, Spring no proporcionará funciones de transacción.

2.1.2 El método se modifica con final y no tendrá efecto

A veces, un método no quiere ser reescrito por subclases, entonces el método puede definirse como final. No hay problema en definir métodos ordinarios como este, pero si el método de transacción se define como final, por ejemplo:

@Service
public class UserService {
    
    

    @Transactional
    public final void add(UserModel userModel){
    
    
        saveData(userModel);
        updateData(userModel);
    }
}

Podemos ver que el método add está definido como final, lo que hará que la transacción falle.

¿Por qué?

Si ha leído el código fuente de las transacciones de primavera, puede saber que aop se usa en la parte inferior de las transacciones de primavera, es decir, a través de proxy dinámico jdk o cglib, nos ayuda a generar clases de proxy e implementar funciones de transacción en clases de proxy. Pero si un método se modifica con final, entonces en su clase de proxy, el método no se puede reescribir para agregar funciones de transacción.

Nota: Si un método es estático, tampoco puede convertirse en un método de transacción a través de un proxy dinámico.

2.1.3 El método en la misma clase se llama directamente internamente, lo que hará que la transacción falle

A veces necesitamos llamar a otro método de transacción en un método de una clase de servicio, como:

@Service
public class UserService {
    
    

    @Autowired
    private UserMapper userMapper;

  
    public void add(UserModel userModel) {
    
    
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
    
    
        doSameThing();
    }
}

Vemos que en el método de transacción add, se llama directamente al método de transacción updateStatus. Del contenido presentado anteriormente, podemos saber que el método updateStatus tiene la capacidad de realizar transacciones porque Spring AOP genera objetos proxy, pero este método llama directamente al método de este objeto, por lo que el método updateStatus no generará transacciones.

Referencia: Referencia 1 ,
Referencia 2

Se puede ver que las llamadas internas directas de métodos en la misma clase harán que la transacción falle.
Entonces, la pregunta es, si en algunos escenarios realmente desea llamar a otro método de sí mismo en un método de la misma clase, ¿qué debe hacer?

Método 1: agregar un nuevo método de servicio
Este método es muy simple, solo necesita agregar un nuevo método de servicio, agregar la anotación @Transactional al nuevo método de servicio y mover el código que requiere la ejecución de transacciones al nuevo método. El código específico es el siguiente:

@Servcie
public class ServiceA {
    
    
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
    
    
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }

 @Servcie
 public class ServiceB {
    
    

    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
    
    
       addData1();
       updateData2();
    }

 }

Método 2: Inyéctate en la clase de Servicio
Si no deseas agregar una nueva clase de Servicio, inyectarte en la clase de Servicio también es una opción. El código específico es el siguiente:

@Servcie
public class ServiceA {
    
    
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
    
    
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
    
    
       addData1();
       updateData2();
    }
 }

Algunas personas pueden tener esta pregunta: ¿Este enfoque causará problemas de dependencia circular?
Respuesta: no.
De hecho, el caché de tres niveles dentro de spring ioc lo garantiza, y no habrá ningún problema de dependencia circular.

Método 3:
use AopContext.currentProxy() para obtener el objeto proxy en la clase de servicio a través de la clase AopContent

De hecho, el método 2 anterior puede resolver el problema, pero el código no parece intuitivo y se puede lograr la misma función usando AOPProxy en la clase Servicio para obtener el objeto proxy. El código específico es el siguiente:

@Servcie
public class ServiceA {
    
    

   public void save(User user) {
    
    
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
    
    
       addData1();
       updateData2();
    }
 }

Nota: informaré un error durante el uso real de este método

No se puede encontrar el proxy actual: establezca la propiedad 'exposeProxy' en Aconsejado en
'verdadero' para

Solución de error: referencia

Antes de hablar sobre las razones y las soluciones para la falla de la transacción Spring, revisemos el modo proxy.Sabemos que las
transacciones declarativas de Spring se basan en el modo proxy. Entonces, antes de hablar sobre la transacción, introduzcamos aproximadamente el modo proxy. De hecho, el modo proxy es bastante simple.
Consiste en envolver otra clase fuera de nuestra clase. Antes de llamar al método que creamos, primero pasaremos por el método externo, realizaremos algunos procesamientos y realizaremos algunas operaciones antes de regresar. Por ejemplo:

    ...
    public User getUserByName(String name) {
     
     
       return userDao.getUserByName(name);
    }
    ... } ```那么如果配置了事务, 就相当于又创建了一个类:

```java public class UserServiceProxy extends UserService{
     
     
    private UserService userService;
    ...
    public User getUserByName(String name){
     
     
        User user = null;
        try{
     
     
            // 在这里开启事务
            user = userService.getUserByName(name);
            // 在这里提交事务
        }
        catch(Exception e){
     
     
            // 在这里回滚事务
            // 这块应该需要向外抛异常, 否则我们就无法获取异常信息了. 
            // 至于方法声明没有添加异常声明, 是因为覆写方法, 异常必须和父类声明的异常"兼容". 
            // 这块应该是利用的java虚拟机并不区分普通异常和运行时异常的特点.
            throw e;
        }
        return user;
    }
    ... } ```然后我们使用的是 UserServiceProxy, 所以就可以”免费”得到事务的支持:

```java @Autowired private UserService userService;    //
这里spring注入的实际上是UserServiceProxy的对象 private void test(){
     
     
    // 由于userService是UserServiceProxy的对象, 所以拥有了事务管理的能力
    userService.getUserByName("aa"); } ```
***Spring事务失效的原因:*** 通过对Spring事务代理模式的分析,我们不难发现Spring事务失效的原因有以下几种情况:
1. privatestaticfinal的使用 、
2. 通过this.xxx()调用当前类的方法
3. 使用默认的事务处理方式
4. 线程Thread中声明式事务不起作用
***Spring事务失效的解决方案:***
 1. privatestaticfinal的使用 这一原因的解决方案很简单,我们只需要:不在类和方法上使用此类关键字即可。
2. 通过this.xxx()调用当前类的方法 这一原因的解决方案如下:

```java @Service public class TaskService {
     
     

@Autowired private TaskManageDAO taskManageDAO; @Transactional public
void test1(){
     
     
    try {
     
             
        this.test2();//这里调用会使事务失效,两条数据都会被保存
        /*           
        原因是:JDK的动态代理。
        在SpringIoC容器中返回的调用的对象是代理对象而不是真实的对象
        只有被动态代理直接调用的才会产生事务。
        这里的this是(TaskService)真实对象而不是代理对象
         */
         //解决方法
        TaskService proxy =(TaskService) AopContext.currentProxy();
        proxy.test2();
    }catch (Exception e){
     
     
        e.printStackTrace();
    }
    Task task = new Task();
    task.setCompleteBy("wjl练习1");
    task.setCompleteTime(new Date());
    taskManageDAO.save(task); } @Transactional(propagation = Propagation.REQUIRES_NEW) // 这个事务的意思是如果前面方法有事务存在,会将前面事务挂起,再重启一个新事务
public void test2(){
     
     
    Task task = new Task();
    task.setCompleteBy("wjl练习2");
    task.setCompleteTime(new Date());
    taskManageDAO.save(task);
    throw new RuntimeException(); } } ```我们仔细看上面的代码会发现:我们使用AopContext.currentProxy()生成了一个当前类的代理类,解决事务失效的问题。
如果使用上述方案报如下异常:Cannot find current proxy: Set 'exposeProxy' property on
Advised to 'true' to make it available,可以采用下面的方案:

```java @Service public class TaskService {
     
     
    @Autowired
    private TaskManageDAO taskManageDAO;
    @Transactional
    public void test1(){
     
     
        try {
     
             
            this.test2();//这里调用会使事务失效,两条数据都会被保存
            /*           
            原因是:JDK的动态代理。
            在SpringIoC容器中返回的调用的对象是代理对象而不是真实的对象
            只有被动态代理直接调用的才会产生事务。
            这里的this是(TaskService)真实对象而不是代理对象
             */
             //解决方法
            getService().test2();
        }catch (Exception e){
     
     
            e.printStackTrace();
        }
        Task task = new Task();
        task.setCompleteBy("wjl练习1");
        task.setCompleteTime(new Date());
        taskManageDAO.save(task);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    // 这个事务的意思是如果前面方法有事务存在,会将前面事务挂起,再重启一个新事务
    public void test2(){
     
     
        Task task = new Task();
        task.setCompleteBy("wjl练习2");
        task.setCompleteTime(new Date());
        taskManageDAO.save(task);
        throw new RuntimeException();
    }
    //解决事务失效
    private TaskService getService(){
     
     
        return SpringUtil.getBean(this.getClass());   //SpringUtil工具类见下面代码
    }  } ```SpringUtil工具类:

```java @Component public class SpringUtil implements
ApplicationContextAware {
     
     
    private static ApplicationContext applicationContext = null;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
     
     
        SpringUtil.applicationContext = applicationContext;
    }
    public static <T> T getBean(Class<T> cla) {
     
     
        return applicationContext.getBean(cla);
    }
    public static <T> T getBean(String name, Class<T> cal) {
     
     
        return applicationContext.getBean(name, cal);
    }
    public static Object getBean(String name){
     
     
        return applicationContext.getBean(name);
    }
    public static String getProperty(String key) {
     
     
        return applicationContext.getBean(Environment.class).getProperty(key);
    } } ```
3. 使用默认的事务处理方式 spring的事务默认是对RuntimeException进行回滚,而不继承RuntimeException的不回滚。因为在java的设计中,它认为不继承RuntimeException的异常是”checkException”或普通异常,如IOException,这些异常在java语法中是要求强制处理的。对于这些普通异常,spring默认它们都已经处理,所以默认不回滚。可以添加rollbackfor=Exception.class来表示所有的Exception都回滚

4. 线程Thread中声明式事务不起作用

```java @Override

public void run() {
     
     
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    PlatformTransactionManager txManager = ContextLoader.getCurrentWebApplicationContext().getBean(PlatformTransactionManager.class);
    TransactionStatus status = txManager.getTransaction(def);
    try {
     
     
           testDao.save(entity);
           txManager.commit(status); // 提交事务
        } catch (Exception e) {
     
     
            System.out.println("异常信息:" + e.toString());
            txManager.rollback(status); // 回滚事务
        }                         } ```

2.1.4 (la clase en sí) no es administrada por spring

En nuestro proceso habitual de desarrollo, hay un detalle que se pasa por alto fácilmente. Es decir, la premisa de usar transacciones Spring es: el objeto debe ser administrado por Spring y se debe crear una instancia de bean.

Por lo general, podemos realizar automáticamente las funciones de creación de instancias de beans e inyección de dependencias a través de @Controller, @Service, @Component, @Repository y otras anotaciones. Por supuesto, hay muchas formas de crear instancias de beans, así que no las mencionaré una por una. Los amigos interesados ​​pueden consultar este artículo: ¿Conoces todas estas coquetas operaciones de @Autowired?

Como se muestra a continuación, se desarrolló una clase de servicio, pero olvidé agregar la anotación @Service, como:

//@Service
public class UserService {
    
    

    @Transactional
    public void add(UserModel userModel) {
    
    
         saveData(userModel);
         updateData(userModel);
    }    
}

En el ejemplo anterior, podemos ver que la clase UserService no está anotada con @Service, entonces la clase no se transferirá a la administración de primavera, por lo que su método de agregar no generará transacciones.

2.1.5 Llamada multihilo

En el desarrollo de proyectos reales, hay muchos escenarios de uso de subprocesos múltiples. ¿Habrá algún problema si las transacciones de primavera se utilizan en escenarios de subprocesos múltiples?

@Slf4j
@Service
public class UserService {
    
    

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
    
    
        userMapper.insertUser(userModel);
        new Thread(() -> {
    
    
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {
    
    

    @Transactional
    public void doOtherThing() {
    
    
        System.out.println("保存role表数据");
    }
}

En el ejemplo anterior, podemos ver que en el método de transacción add, se llama al método de transacción doOtherThing, pero el método de transacción doOtherThing se llama en otro subproceso.

Esto hará que los dos métodos no estén en el mismo hilo, y las conexiones de la base de datos obtenidas serán diferentes, por lo tanto, dos transacciones diferentes. Si desea lanzar una excepción en el método doOtherThing, es imposible que el método add retroceda.

Si ha visto el código fuente de las transacciones de primavera, puede saber que las transacciones de primavera se implementan a través de conexiones de base de datos. Se guarda un mapa en el subproceso actual, la clave es la fuente de datos y el valor es la conexión a la base de datos.

private static final ThreadLocal<Map<Object, Object>> resources =

  new NamedThreadLocal<>("Transactional resources");


La misma transacción de la que estamos hablando en realidad se refiere a la misma conexión de base de datos. Solo la misma conexión de base de datos puede confirmarse y revertirse al mismo tiempo. Si está en diferentes hilos, la conexión a la base de datos obtenida debe ser diferente, por lo que es una transacción diferente.

2.1.6 6. Las tablas (motor de almacenamiento) no admiten transacciones

Como todos sabemos, antes de mysql5, el motor de base de datos predeterminado es myisam.

No hace falta decir sus beneficios: los archivos de índice y los archivos de datos se almacenan por separado, y para operaciones de una sola tabla con más consultas y menos escrituras, el rendimiento es mejor que InnoDB.

En algunos proyectos antiguos, todavía se puede utilizar.

Al crear una tabla, solo necesita configurar el parámetro ENGINE en MyISAM:

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

myisam es fácil de usar, pero hay un problema fatal: no admite transacciones.

Si es solo una operación de una sola tabla, está bien y no habrá demasiados problemas. Pero si necesita operar en varias tablas, porque no admite transacciones, es probable que los datos estén incompletos.

Además, myisam no admite bloqueos de fila ni claves foráneas.

Entonces, en escenarios comerciales reales, myisam no se usa mucho. Después de mysql5, myisam se ha retirado gradualmente del escenario de la historia, reemplazado por innodb.

A veces, durante el proceso de desarrollo, encontramos que la transacción de una determinada tabla no ha tenido efecto. No es necesariamente culpa de la transacción de primavera. Es mejor confirmar si la tabla que está utilizando admite transacciones.

2.1.7 Transacción sin abrir

A veces, la causa principal de que una transacción no surta efecto es que la transacción no se ha abierto.

Usted puede encontrar esta frase divertida.

¿No es abrir una transacción la función más básica en un proyecto?

¿Por qué no hay transacción todavía?

Así es, si el proyecto ha sido construido, debe haber funciones de transacción.

Pero si está creando una demostración de proyecto, solo hay una tabla y la transacción de esta tabla no tiene efecto. Entonces, ¿cuál podría ser la causa?

Por supuesto, hay muchas razones, pero la razón para no abrir la transacción es extremadamente fácil de ignorar.

Si está utilizando el proyecto springboot, entonces tiene suerte. Porque springboot ha abierto silenciosamente la transacción por usted a través de la clase DataSourceTransactionManagerAutoConfiguration.

Lo que tienes que hacer es muy simple, solo necesitas configurar los parámetros relacionados con spring.datasource.

Pero si todavía usa un proyecto Spring tradicional, debe configurar manualmente los parámetros relacionados con la transacción en el archivo applicationContext.xml. Si olvida configurar, la transacción definitivamente no tendrá efecto.

La información de configuración específica es la siguiente:

<!-- 配置事务管理器 --> 
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- 用切点把事务切进去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 


Hablando en silencio, si las reglas de coincidencia del punto de entrada en la etiqueta pointcut coinciden incorrectamente, algunos tipos de transacciones no tendrán efecto.

3. La transacción no retrocede [cinco tipos]

3.1 Características de propagación de errores

De hecho, cuando usamos la anotación @Transactional, podemos especificar el parámetro de propagación.

La función de este parámetro es especificar las características de propagación de la transacción. Spring actualmente admite 7 características de propagación:

OBLIGATORIO Si hay una transacción en el contexto actual, únase a la transacción; si no hay transacción, cree una transacción, que es el valor del atributo de propagación predeterminado.
SOPORTES Si hay una transacción en el contexto actual, admite la transacción para unirse a la transacción, si no hay transacción, se ejecuta de manera no transaccional.
OBLIGATORIO si hay una transacción en el contexto actual, de lo contrario lanzar una excepción.
REQUIRES_NEW crea una nueva transacción cada vez y, al mismo tiempo, suspende la transacción en el contexto. Una vez que se completa la ejecución de la nueva transacción actual, la transacción del contexto se reanuda y se ejecuta de nuevo.
NOT_SUPPORTED Si hay una transacción en el contexto actual, la transacción actual se suspende y luego se ejecuta el nuevo método en un entorno sin transacción.
NUNCA arroja una excepción si hay una transacción en el contexto actual; de lo contrario, el código se ejecuta en un entorno sin transacciones.
NESTED Si existe una transacción en el contexto actual, se ejecuta la transacción anidada, y si no hay transacción, se crea una nueva transacción.
Si configuramos incorrectamente las características de propagación cuando configuramos manualmente los parámetros de propagación, por ejemplo:

@Service
public class UserService {
    
    

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
    
    
        saveData(userModel);
        updateData(userModel);
    }
}

Podemos ver que la función de propagación de transacciones del método add se define como Propagation.NEVER.Este tipo de función de propagación no admite transacciones y se lanzará una excepción si hay una transacción.

Solo estas tres características de propagación actualmente crean nuevas transacciones:REQUIRED,REQUIRES_NEW,NESTED。

3.2 Tragando la excepción usted mismo

La transacción no se revertirá. El problema más común es que el desarrollador intente manualmente... capturar la excepción en el código. Por ejemplo:

@Slf4j
@Service
public class UserService {
    
    
    
    @Transactional
    public void add(UserModel userModel) {
    
    
        try {
    
    
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
        }
    }
}

En este caso, la transacción de primavera, por supuesto, no se revertirá, porque el desarrollador detectó la excepción y no la lanzó manualmente, en otras palabras, se tragó la excepción.

Si desea que la transacción de primavera retroceda normalmente, debe generar una excepción que pueda manejar. Si no se lanza ninguna excepción, Spring considera que el programa es normal.

3.3 Lanzar manualmente otras excepciones

Incluso si el desarrollador no detecta manualmente la excepción, si la excepción lanzada es incorrecta, la transacción de primavera no se revertirá.

@Slf4j
@Service
public class UserService {
    
    
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
    
    
        try {
    
    
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

En la situación anterior, el desarrollador detectó la excepción y luego lanzó manualmente la excepción: Excepción, y la transacción no se revertirá.

Debido a la transacción de primavera, solo RuntimeException (excepción de tiempo de ejecución) y Error (error) se revertirán de forma predeterminada. Para la excepción ordinaria (excepción que no es de tiempo de ejecución), no se revertirá. Como IOExeption común y SQLException

3.4 Excepción de reversión personalizada

Cuando usamos la anotación @Transactional para declarar una transacción, a veces queremos personalizar la excepción de la reversión, y Spring también lo admite. Esta función se puede lograr configurando el parámetro rollbackFor.

Pero si el valor de este parámetro está mal configurado, dará lugar a algunos problemas inexplicables, como:

@Slf4j
@Service
public class UserService {
    
    
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
    
    
       saveData(userModel);
       updateData(userModel);
    }
}

Si se ejecuta el código anterior, al guardar y actualizar datos, el programa informa un error y lanza SqlException, DuplicateKeyException y otras excepciones. Y BusinessException es nuestra excepción personalizada, y la excepción informada no pertenece a BusinessException, por lo que la transacción no se revertirá.

Aunque rollbackFor tiene un valor predeterminado, los desarrolladores aún deben volver a especificar este parámetro en la especificación para desarrolladores de Alibaba.

¿Por qué es esto?

Porque si se usa el valor predeterminado, una vez que el programa lanza una excepción, la transacción no se revertirá, lo que causará un gran error. Por lo tanto, se recomienda establecer este parámetro en: Excepción o Throwable en circunstancias normales.

3.5 Hay demasiadas reversiones de transacciones anidadas

public class UserService {
    
    

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
    
    
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {
    
    

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
    
    
        System.out.println("保存role表数据");
    }
}

En este caso, se usa una transacción interna anidada.Originalmente, cuando ocurre una excepción al llamar al método roleService.doOtherThing, solo se revertirá el contenido del método doOtherThing, y el contenido en userMapper.insertUser no se revertirá, es decir, el punto de guardado se revertirá. Pero la cuestión es que insertUser también se revierte.

¿por qué?

Debido a que hay una excepción en el método doOtherThing, seguirá apareciendo sin captura manual, y la excepción se capturará en el método proxy del método de adición externo. Por lo tanto, en este caso, toda la transacción se retrotrae directamente, no solo un único punto de guardado.

¿Cómo puedo revertir el punto de guardado?

@Slf4j
@Service
public class UserService {
    
    

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
    
    

        userMapper.insertUser(userModel);
        try {
    
    
            roleService.doOtherThing();
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
        }
    }
}

Puede poner la transacción anidada interna en try/catch, y no continúe lanzando excepciones. Esto garantiza que si se produce una excepción en la transacción anidada interna, solo se revierte la transacción interna sin afectar la transacción externa.

4. Problemas principales

Cuando se usan transacciones de primavera, hay un problema muy problemático, que es el problema de las transacciones grandes.
Para asuntos importantes, consulte: Referencia

Por lo general, anotaremos @Transactional en el método y agregaremos funciones de transacción, como:

@Service
public class UserService {
    
    
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
    
    
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {
    
    
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
    
    
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

Pero la anotación @Transactional, si se agrega al método, tiene la desventaja de que todo el método se incluye en la transacción.

En el ejemplo anterior, en la clase UserService, solo estas dos líneas requieren transacciones:

roleService.save(userModel);
update(userModel);

En la clase RoleService, solo esta línea requiere una transacción:

saveData(userModel);

La forma actual de escritura hará que todos los métodos de consulta se incluyan en la misma transacción.

Si hay muchos métodos de consulta, el nivel de llamada es muy profundo y algunos métodos de consulta consumen mucho tiempo, la transacción completa requerirá mucho tiempo, lo que generará grandes problemas de transacción.

Supongo que te gusta

Origin blog.csdn.net/u014212540/article/details/129623865
Recomendado
Clasificación