概要
为什么会出现分布式事务问题?
一个springcloud微服务,每个springboot项目如果部署在多个服务器上,且项目中开启有定时器定时统计数据,并将数据进行入库或更新,如果多个服务器同时运行会造成数据库重复数据或者数据缺失问题吗
在Spring Boot项目中,如果部署在多个服务器上并且项目中有定时器定时统计数据并进行数据库操作(如入库或更新),确实存在数据重复或数据缺失的风险。这是因为每个服务器上的应用实例都会尝试执行相同的定时任务,导致对数据库的操作可能会重复执行或者相互冲突。
为了解决这个问题,可以采取以下策略之一:
-
使用分布式定时任务框架:如Quartz、ElasticJob、XXL-Job等,这些框架支持分布式环境下的任务调度,能够确保在多个实例中只有一个实例执行定时任务。
-
基于数据库的锁:在执行定时任务之前,尝试在数据库中获取一个锁。只有获取锁的实例才能执行任务,执行完毕后释放锁。这种方式简单但可能会有性能瓶颈。
-
使用Spring Cloud的分布式锁:如果你的项目已经在使用Spring Cloud,可以利用Spring Integration提供的分布式锁支持,或者使用外部系统如Redis、Zookeeper来实现分布式锁。
-
定时任务调度中心:使用专门的调度中心来管理所有的定时任务,如Spring Cloud Task、Apache Airflow等。这些调度中心可以负责任务的分配和执行,确保任务不会在多个实例中重复执行。
-
使用@Scheduled注解的fixedDelay或fixedRate属性:通过这些属性可以简单地控制任务执行的频率,尽管这不直接解决多实例下的任务重复执行问题,但可以通过逻辑上确保任务执行的幂等性来间接避免问题
整体架构流程
目前项目有spring cloud config,spring cloud gateway,xxx-schedule-service等服务,部署上线后xxx-schedule-service为了保证数据的实时性统计需要将该服务部署在不同服务器上,定时器服务采用Spring的@Scheduled进行任务调度。
服务集成了redis,Quartz进行分布式任务框架,Quartz服务需要使用到mysql服务,所以相当于Quartz使用到的数据也同样具有ACID。
使用Quartz实现分布式任务调度的关键在于让Quartz的多个实例共享同一个作业存储(JobStore),这样,即使在多个服务器上部署了应用,它们也能协调作业的执行,确保同一个作业不会在多个实例上重复执行。以下是实现步骤的概述:
-
选择合适的JobStore:Quartz提供了几种JobStore,对于分布式任务调度,通常使用
JobStoreTX
或JobStoreCMT
(用于JTA环境)来支持数据库存储,或者JobStoreTerracotta
来支持Terracotta服务器。 -
配置数据源:Quartz使用数据库来存储作业(Job)和触发器(Trigger)的信息,因此需要配置数据源。这通常涉及到在Quartz配置文件中指定数据库连接信息。
-
配置Quartz属性:在Quartz的配置文件(如
quartz.properties
)中,设置org.quartz.jobStore.class
为你选择的JobStore实现,配置数据库连接属性,并设置其他相关属性,如集群属性。 -
初始化Scheduler:在应用中初始化Quartz的
Scheduler
,确保它使用的是你配置的quartz.properties
文件。 -
配置集群属性:在
quartz.properties
中配置集群相关的属性,如org.quartz.jobStore.isClustered
设置为true
,以及集群检查间隔org.quartz.jobStore.clusterCheckinInterval
等。 -
启动和管理作业:定义作业(Job)和触发器(Trigger),然后使用Scheduler安排它们。Quartz会自动处理分布式环境下的作业调度和执行。
org.quartz.scheduler.instanceName = MyClusteredScheduler org.quartz.scheduler.instanceId = AUTO org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.dataSource = myDS org.quartz.jobStore.tablePrefix = QRTZ_ org.quartz.jobStore.isClustered = true org.quartz.jobStore.clusterCheckinInterval = 20000 org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz?useSSL=false org.quartz.dataSource.myDS.user = quartz org.quartz.dataSource.myDS.password = quartz org.quartz.dataSource.myDS.maxConnections = 5 org.quartz.threadPool.threadCount = 3
技术名词解释
Redis分布式锁:添加依赖:首先,确保你的项目中包含了Spring Boot Redis的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
实现锁逻辑:使用Redis的原子操作(如SET key value NX PX milliseconds
)来实现锁的获取和释放。
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.concurrent.TimeUnit;
public class RedisLock {
private StringRedisTemplate template;
public RedisLock(StringRedisTemplate template) {
this.template = template;
}
public boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
ValueOperations<String, String> ops = template.opsForValue();
Boolean success = ops.setIfAbsent(key, value, timeout, unit);
return Boolean.TRUE.equals(success);
}
public void unlock(String key, String value) {
String currentValue = template.opsForValue().get(key);
if (value.equals(currentValue)) {
template.delete(key);
}
}
}
使用Zookeeper实现分布式锁:
添加依赖:引入Apache Curator,它是一个Zookeeper客户端框架,简化了Zookeeper的操作。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.1.0</version>
</dependency>
配置Zookeeper客户端:创建并配置一个CuratorFramework
实例。
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
public class ZookeeperConfig {
public CuratorFramework curatorFramework() {
return CuratorFrameworkFactory.builder()
.connectString("localhost:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
}
}
实现锁逻辑:使用Curator提供的InterProcessMutex
来实现分布式锁。
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
public class ZookeeperLock {
private InterProcessMutex lock;
public ZookeeperLock(CuratorFramework client, String lockPath) {
this.lock = new InterProcessMutex(client, lockPath);
}
public void lock() throws Exception {
lock.acquire();
}
public void unlock() throws Exception {
lock.release();
}
}
在这两种方法中,你可以根据自己的项目需求和环境选择最适合的一种。Redis提供了快速的性能和简单的操作,适合轻量级锁和高速缓存场景。Zookeeper提供了更强的一致性保证,适合对一致性要求较高的场景。
技术细节
1.如果是springcloud的Zookeeper锁,我这里有两个定时器服务在两个服务器上,同时执行,那这个zookeeper锁怎么使用?
在Spring Cloud环境中,使用Zookeeper作为分布式锁的存储和管理机制,可以确保即使有多个服务实例(可能部署在不同的服务器上)尝试执行相同的定时任务,也只有一个实例能够获得执行权限。以下是一个基于Spring Cloud和Zookeeper实现分布式锁以控制定时任务执行的示例步骤:
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ScheduledTasksWithLock {
private final CuratorFramework client;
private final InterProcessMutex lock;
@Autowired
public ScheduledTasksWithLock(CuratorFramework client) {
this.client = client;
// 指定锁路径
this.lock = new InterProcessMutex(client, "/locks/my-lock");
}
@Scheduled(fixedRate = 5000)
public void executeTask() {
try {
// 尝试获取锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 执行任务逻辑
System.out.println("执行定时任务: " + LocalDateTime.now());
} finally {
// 释放锁
lock.release();
}
} else {
System.out.println("未能获取锁,跳过此次任务执行: " + LocalDateTime.now());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
如果你在使用Spring Cloud环境下的多个服务实例来执行定时任务,并希望通过Zookeeper实现分布式锁以确保任务在任一时刻只被一个实例执行,那么你需要在每个服务实例中都进行Zookeeper的相关配置。这样,每个服务实例都能够连接到同一个Zookeeper集群,通过Zookeeper集群来协调锁的获取与释放,从而实现分布式锁的功能。
2.这样只是保证了多个定时器任务在同一个时刻只有一个能执行,但是如果一个定时器任务执行完插入sql操作,释放锁,另一个定时器执行,同样sql操作,但是上一个插入操作数据库还没完全插入完,这时候是不是会造成重复数据了
即使使用了分布式锁来保证在任一时刻只有一个定时器任务在执行,但如果任务执行涉及到数据库操作(如插入操作),且这些操作没有在获取锁的代码块内原子性地完成,就可能出现你所描述的情况:一个任务释放了锁,但其数据库操作尚未完全完成,另一个任务随即获取锁并开始执行,可能会导致数据的重复插入或其他数据一致性问题。
为了避免这种情况,你可以采取以下措施:
-
确保数据库操作的原子性:将数据库操作包含在获取锁和释放锁的代码块内,确保这些操作作为一个整体原子性地执行。这意味着,在释放锁之前,所有数据库操作都已成功提交并完成。
-
使用数据库事务:如果你的数据库操作包含多个步骤,考虑使用数据库事务来确保这些步骤要么全部成功,要么在遇到错误时全部回滚。这有助于保持数据的一致性。
-
检查数据唯一性:在执行插入操作之前,检查数据库中是否已存在相同的数据。这可以通过在数据库中设置唯一约束或在插入前执行查找操作来实现。
-
使用乐观锁或悲观锁:在数据库层面,通过乐观锁或悲观锁机制来控制并发访问,从而避免数据一致性问题。乐观锁适用于写冲突较少的场景,而悲观锁适用于写冲突较多的场景。
3.如果使用redis实现分布式锁需要注意什么
- 锁的唯一性:确保任何时刻,对于同一个资源,只有一个客户端能获取到锁。这通常通过使用
SETNX
(SET if Not eXists)命令实现,该命令只有在键不存在时才设置键值。 - 锁的超时机制:为避免客户端在获取锁后因崩溃或其他原因未能释放锁,导致资源永远被锁定,需要为锁设置一个超时时间。这可以通过
EXPIRE
命令实现,或者在使用SET
命令时一起设置超时时间。 - 锁的安全释放:只有锁的持有者才能释放锁,以避免一个客户端错误地释放了另一个客户端持有的锁。这通常通过在设置锁时附加一个唯一标识符(如UUID),并在释放锁时检查该标识符实现。
- 防止死锁:即使设置了超时时间,也应在客户端实现逻辑以防止死锁情况的发生,比如通过设置合理的重试逻辑。
- 考虑RedLock算法:对于需要更高可靠性的场景,可以考虑使用RedLock算法。RedLock算法是由Redis的创造者提出的,用于在多个Redis实例上实现分布式锁,以提高容错性
小结
分布式事务涉及到的细节很多,需要根据不同项目业务中所需要的分布式锁进行控制,防止头重脚轻的硬搬硬套这样只会使项目的资源消耗更大,带来的后果也越严重