Desde la perspectiva del código fuente, analice el esquema de reversión de excepción de datos bajo la condición de concurrencia de subprocesos múltiples

1. Solución de reversión de excepción de datos en el caso de concurrencia de subprocesos múltiples

En el caso de requerir múltiples operaciones de datos sin secuencia, generalmente podemos optar por usar operaciones concurrentes para mejorar la velocidad de procesamiento, pero en casos concurrentes, ¿podemos aún resolver el problema de la reversión de transacciones @Transactional?

Por ejemplo, la siguiente estructura de tabla:

CREATE TABLE `test` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `thread_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Si se necesitan realizar dos operaciones de escritura y no hay un orden de escritura, podemos abrir un hilo para escribir simultáneamente. Aquí tomamos la operación como ejemplo. JdbcTemplateUsar otras DBherramientas tiene el mismo efecto, por ejemplo:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
    }
}

inserte la descripción de la imagen aquí

La base de datos ha escrito correctamente dos datos. Si se produce una excepción durante otras operaciones:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        int a = 1 / 0;
    }
}

Después de ejecutar, puede ver que se ha lanzado una excepción:
inserte la descripción de la imagen aquí

Consulta la base de datos:

inserte la descripción de la imagen aquí

Se encuentra que una parte de los datos todavía está escrita, y la operación en el hilo no se revierte, pero el hilo principal se revierte. Dado que uno se revierte y el otro no se revierte, no debe usar la misma conexión a la base de datos. Aquí está el código fuente para ver dónde obtener la conexión a la base de datos JdbcTemplate:

Ingrese el método JdbcTemplatede update(String sql, @Nullable Object... args):
inserte la descripción de la imagen aquí
se llama el método de la clase actual update(String sql, @Nullable PreparedStatementSetter pss), y finalmente se llama el método de la clase actual update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss):
inserte la descripción de la imagen aquí

El método se usa principalmente aquí execute(StatementCallback<T> action), ingrese este método:

inserte la descripción de la imagen aquí

Se puede ver aquí DataSourceUtils.getConnectionque la conexión a la base de datos se obtiene a través del método, e ingrese este método:

inserte la descripción de la imagen aquí

TransactionSynchronizationManager¿Es un poco familiar verlo aquí ? @TransactionalAl explicar el análisis del código fuente de las transacciones declarativas anteriormente en esta columna, la lógica de iniciar la transacción es usar TransactionSynchronizationManagerla conexión de la base de datos obtenida. Si no entiende esta parte, puede leer el siguiente artículo:

Análisis de código fuente de SpringTx: principio de ejecución de transacción declarativa @Transactional

De hecho, en Stringla ecología, el acceso a las conexiones de la base de datos se usa básicamente por defecto TransactionSynchronizationManager.

Veamos también @Transactionalla lógica de obtener la conexión cuando la transacción se inicia bajo la anotación, bajo el DataSourceTransactionManagersiguiente doGetTransactionmétodo:

inserte la descripción de la imagen aquí

Puede ver que también se usa aquí TransactionSynchronizationManagerpara obtener la conexión.

Veamos TransactionSynchronizationManagerlo que ha hecho e ingrese getResourceel método:

inserte la descripción de la imagen aquí

Aquí el método se activa de nuevo doGetResourcey entra bajo este método:

inserte la descripción de la imagen aquí

Aquí obviamente se obtiene resourcesde , veamos resourcesqué queda al final:

inserte la descripción de la imagen aquí

Es uno ThreadLocal, ¿comprende ahora que en ausencia de subprocesos múltiples, cuando se inicia la transacción, la conexión obtenida se coloca en el actual, y otros componentes realizan operaciones de datos más tarde, y la conexión también se obtiene primero de, de modo que todas las operaciones se realizan en una conexión y, naturalmente, se puede revertir. Dado que abrimos el subproceso por separado anteriormente, la operación en el subproceso intentó obtener la conexión en curso, pero no pudo obtenerla, por lo que solo pudimos obtener una nueva operación de conexión, lo que resultó en la inconsistencia entre la conexión cuando de aclarar la transacción y la conexión durante la operación real, por lo que no se ThreadLocalpuede ThreadLocalrevertir ThreadLocal.

Ahora que hemos encontrado la causa del problema, ¿cómo podemos solucionarlo?

Dado que ThreadLocalla conexión es diferente debido a , cuando comenzamos el hilo, le agregaremos cierta información. Se usa para obtener la conexión TransactionSynchronizationManager, por lo que también se usa para agregarla TransactionSynchronizationManager. Al observar TransactionSynchronizationManager, Apipuede usar para obtener el identificador de conexión:

ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

Entre ellos keyestá la fuente de datos actual, se puede usar el identificador de enlace:

 TransactionSynchronizationManager.bindResource(dataSource, conHolder);

El mango de extracción se puede utilizar:

TransactionSynchronizationManager.unbindResource(dataSource);

Modifiquemos el programa anterior:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        int a = 1 / 0;
    }
}

corre de nuevo:

inserte la descripción de la imagen aquí

Ha ocurrido una excepción, verifique la base de datos:

inserte la descripción de la imagen aquí

¡Los datos se revirtieron con éxito!

¿Puede aparecer la excepción falsa en el subproceso secundario y puede revertirse? Empecemos el experimento:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture.runAsync(() -> {
    
    
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
            int a = 1 / 0;
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
    }
}

Después de ejecutar, ver los datos:

inserte la descripción de la imagen aquí
Se encuentra que no hay un fenómeno de reversión. Esto se debe a que la excepción está en el subproceso secundario Runnabley el subproceso principal no percibe la excepción. ¿Cómo hacer que el subproceso principal la perciba? Podemos agregar uno al final del procesamiento de datos. joinSi hay otra excepción, se lanzará al subproceso principal:

@Service
public class TestService {
    
    

    @Resource
    JdbcTemplate jdbcTemplate;

    @Resource
    DataSource dataSource;

    @Transactional(rollbackFor = Exception.class)
    public void test() {
    
    
        // 获取当前线程的句柄
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 放入子线程
        CompletableFuture future = CompletableFuture.runAsync(() -> {
    
    
            // 子线程绑定
            TransactionSynchronizationManager.bindResource(dataSource, conHolder);
            jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                    , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
            int a = 1 / 0;
            // 解绑
            TransactionSynchronizationManager.unbindResource(dataSource);
        });
        // ....其他操作...
        jdbcTemplate.update("insert into test(name,thread_name) value(? , ?)"
                , new Object[]{
    
    LocalDateTime.now().toString(), Thread.currentThread().getName()});
        // ....其他操作...
        future.join();
    }
}

Después de ejecutar, puede ver que se ha lanzado una excepción:

inserte la descripción de la imagen aquí

Consulta la base de datos:

inserte la descripción de la imagen aquí

Los datos también se revirtieron con éxito.

2. Extensión: el subproceso secundario de MVC obtiene Solicitar información

Después de leer el proceso de transacción anterior, de la misma manera, en MVC, si se ejecuta originalmente en el subproceso principal, hay una demanda para optimizar el subproceso más adelante, pero se obtiene información de ThreadLocalél Request:

@RestController
@RequestMapping("/test3")
public class RequestController {
    
    

    @GetMapping("/test")
    public void test(){
    
    
        // 获取句柄
        ServletRequestAttributes att = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        CompletableFuture.runAsync(()->{
    
    
            // 绑定
            RequestContextHolder.setRequestAttributes(att);

            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes();

            HttpServletRequest request = attributes.getRequest();
            System.out.println(request.getHeader("token"));
            // 解绑
            RequestContextHolder.resetRequestAttributes();
        }).join();
    }
}

Supongo que te gusta

Origin blog.csdn.net/qq_43692950/article/details/130914163
Recomendado
Clasificación