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. JdbcTemplate
Usar otras DB
herramientas 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()});
// ....其他操作...
}
}
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:
Consulta la base de datos:
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 JdbcTemplate
de update(String sql, @Nullable Object... args)
:
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)
:
El método se usa principalmente aquí execute(StatementCallback<T> action)
, ingrese este método:
Se puede ver aquí DataSourceUtils.getConnection
que la conexión a la base de datos se obtiene a través del método, e ingrese este método:
TransactionSynchronizationManager
¿Es un poco familiar verlo aquí ? @Transactional
Al 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 TransactionSynchronizationManager
la conexión de la base de datos obtenida. Si no entiende esta parte, puede leer el siguiente artículo:
De hecho, en String
la ecología, el acceso a las conexiones de la base de datos se usa básicamente por defecto TransactionSynchronizationManager
.
Veamos también @Transactional
la lógica de obtener la conexión cuando la transacción se inicia bajo la anotación, bajo el DataSourceTransactionManager
siguiente doGetTransaction
método:
Puede ver que también se usa aquí TransactionSynchronizationManager
para obtener la conexión.
Veamos TransactionSynchronizationManager
lo que ha hecho e ingrese getResource
el método:
Aquí el método se activa de nuevo doGetResource
y entra bajo este método:
Aquí obviamente se obtiene resources
de , veamos resources
qué queda al final:
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 ThreadLocal
puede ThreadLocal
revertir ThreadLocal
.
Ahora que hemos encontrado la causa del problema, ¿cómo podemos solucionarlo?
Dado que ThreadLocal
la 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
, Api
puede usar para obtener el identificador de conexión:
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
Entre ellos key
está 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:
Ha ocurrido una excepción, verifique la base de datos:
¡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:
Se encuentra que no hay un fenómeno de reversión. Esto se debe a que la excepción está en el subproceso secundario Runnable
y 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. join
Si 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:
Consulta la base de datos:
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();
}
}