Implementación de base de datos de bloqueo distribuido
Que es un candado distribuido
En un sistema de un solo proceso de instancia única, cuando varios subprocesos modifican una variable compartida al mismo tiempo, para garantizar la seguridad de los subprocesos, es necesario sincronizar la variable o el código. Este tipo de operación de sincronización puede utilizar paquetes JUC y sincronizados en Java. Bajo el bloqueo explícito, cas + volátil para lograr.
En la actualidad, la mayoría de los sistemas se implementan de manera distribuida. El uso manual, como el sincronizado, solo puede garantizar la seguridad de los subprocesos dentro de un único proceso. La seguridad de los subprocesos en múltiples procesos y múltiples instancias requiere bloqueos distribuidos.
Actualmente existen cuatro soluciones principales de bloqueo distribuido:
- Basado en implementación de base de datos (pesimista + optimista)
- Basado en Redis
- Basado en ZooKeeper
- Basado en Etcd
El bloqueo pesimista de la base de datos se da cuenta del bloqueo distribuido
Crear tabla sql:
CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`amount` int(11) NOT NULL,
`status` int(11) NOT NULL,
`version` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
INSERT INTO `t_order` (`amount`, `status`, `version`) VALUES ('100', '1', '1');
Primero use sql para simular la realización de bloqueos distribuidos:
paso | SesiónA | SesiónB |
---|---|---|
1 | empezar; | empezar; |
2 | seleccione * de t_order donde id = 1 para actualizar; | |
3 | seleccione * de t_order donde id = 1 para actualizar; - 阻塞 | |
4 | actualizar t_order set status = 2 donde id = 1 y status = 1; | |
5 | cometer; | Devolver el resultado de la consulta |
6 | update t_order set status = 2 donde id = 1 y status = 1; - el estado ha cambiado pero la actualización no es exitosa | |
7 | cometer; |
Descripción:
- El cliente A y el cliente B ejecutan las dos primeras filas de sql al mismo tiempo, el cliente A devuelve datos y el cliente B bloquea a la espera de que se adquiera el bloqueo de fila.
- El cliente A ejecuta las siguientes dos filas de SQL, confirma la transacción y el cliente B obtiene el bloqueo de fila e inmediatamente devuelve los datos.
- El cliente B ejecuta las siguientes dos líneas de SQL, confirma la transacción y libera el bloqueo de fila.
Tenga en cuenta que la
for update
declaración debe tomar el índice de la clave principal; de lo contrario, toda la tabla se bloqueará si no se toma el índice, y se generarán bloqueos de espacios si se toman otros índices, que pueden bloquear múltiples registros.
Implementación del código Java:
package com.morris.distribute.lock.database.exclusive;
import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 数据库分布式锁之悲观锁
*
* @param id
*/
@Transactional
public void updateStatus(int id) {
log.info("updateStatus begin, {}", id);
Integer status = jdbcTemplate.queryForObject("select status from t_order where id=? for update", new Object[]{
id}, Integer.class);
if (Order.ORDER_STATUS_NOT_PAY == status) {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
int update = jdbcTemplate.update("update t_order set status=? where id=? and status=1", new Object[]{
2, id, Order.ORDER_STATUS_NOT_PAY});
if (update > 0) {
log.info("updateStatus success, {}", id);
} else {
log.info("updateStatus failed, {}", id);
}
} else {
log.info("updateStatus status already updated, ignore this request, {}", id);
}
log.info("updateStatus end, {}", id);
}
}
Preste atención a la apertura de la transacción @Transactional
.
Utilice varios subprocesos para simular bloqueos en competencia:
package com.morris.distribute.lock.database.exclusive;
import com.morris.distribute.config.JdbcConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.stream.IntStream;
public class Demo {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(JdbcConfig.class);
applicationContext.register(OrderService.class);
applicationContext.refresh();
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
IntStream.rangeClosed(1, 3).forEach((i) -> new Thread(() -> {
OrderService orderService = applicationContext.getBean(OrderService.class);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
orderService.updateStatus(1);
}, "t" + i).start());
}
}
Los resultados son los siguientes:
2020-09-16 14:16:53,248 INFO [t2] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248 INFO [t1] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:53,248 INFO [t3] (OrderService.java:26) - updateStatus begin, 1
2020-09-16 14:16:56,289 INFO [t2] (OrderService.java:42) - updateStatus success, 1
2020-09-16 14:16:56,289 INFO [t2] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,290 INFO [t3] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,290 INFO [t3] (OrderService.java:49) - updateStatus end, 1
2020-09-16 14:16:56,291 INFO [t1] (OrderService.java:47) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:16:56,291 INFO [t1] (OrderService.java:49) - updateStatus end, 1
Se puede ver en los resultados de ejecución que solo un subproceso mantiene el bloqueo al mismo tiempo.
El bloqueo optimista de la base de datos se da cuenta del bloqueo distribuido
El bloqueo optimista utiliza el número de versión para determinar si el registro se ha actualizado cada vez.
package com.morris.distribute.lock.database.share;
import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 数据库分布式锁之乐观锁
*
* @param id
*/
public void updateStatus(int id) {
log.info("updateStatus begin, {}", id);
for (;;) {
// 自旋,有可能对订单做其他操作,导致version变了,所以需要自旋
Order order = jdbcTemplate.queryForObject("select status, version from t_order where id=?",
new Object[]{
id}, (rs, row) -> {
Order o = new Order();
o.setStatus(rs.getInt(1));
o.setVersion(rs.getInt(2));
return o;
});
if (Order.ORDER_STATUS_NOT_PAY == order.getStatus()) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
int update = jdbcTemplate.update("update t_order set status=?,version=? where id=? and version=? and status=?",
new Object[]{
Order.ORDER_STATUS_PAY_SUCCESS, order.getVersion() + 1, id, order.getVersion(), Order.ORDER_STATUS_NOT_PAY});
if (update > 0) {
log.info("updateStatus success, {}", id);
break;
} else {
log.info("updateStatus failed, {}", id);
}
} else {
log.info("updateStatus status already updated, ignore this request, {}", id);
break;
}
}
log.info("updateStatus end, {}", id);
}
}
Los resultados son los siguientes:
2020-09-16 14:21:08,934 INFO [t3] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934 INFO [t2] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:08,934 INFO [t1] (OrderService.java:25) - updateStatus begin, 1
2020-09-16 14:21:12,110 INFO [t1] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,110 INFO [t2] (OrderService.java:50) - updateStatus failed, 1
2020-09-16 14:21:12,111 INFO [t3] (OrderService.java:47) - updateStatus success, 1
2020-09-16 14:21:12,111 INFO [t3] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117 INFO [t2] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117 INFO [t1] (OrderService.java:53) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:21:12,117 INFO [t1] (OrderService.java:57) - updateStatus end, 1
2020-09-16 14:21:12,117 INFO [t2] (OrderService.java:57) - updateStatus end, 1
para resumir
Bloqueo pesimista: bloqueará toda la fila de registros, lo que resultará en la imposibilidad de realizar otras operaciones comerciales en los datos, y es ineficiente. Si el sql no está bien escrito, puede producirse un bloqueo de brechas, bloqueando varios registros o incluso toda la tabla.
Bloqueo optimista: cada tabla debe agregar un campo de versión que no tiene nada que ver con el negocio.
Ventajas: Basado directamente en la implementación de la base de datos, fácil de implementar.
Desventajas: la sobrecarga de E / S es grande, el número de conexiones es limitado y no puede satisfacer las necesidades de alta concurrencia.