## 什么是Check -DoSomething?
在日常开发中,我们经常会遇到一种代码模式,即先检查某个条件(如数据项是否存在),然后根据检查结果执行相应的操作(如插入新记录或更新已有记录)。这种模式通常被称为“Check-DoSomething”问题。看似简单的逻辑,但在实际应用中却容易引发各种问题,而且在测试过程中,也能难被测试验证到。本文将通过真实案例,深入探讨这些问题,并提供有效的解决方案。
## Check-DoSomething 有什么问题呢?
下面我将通过2个真实的案例给大家分享一下Check-DoSomething 的问题,
案例一:MQ消息批量消费导致的重复入库
在处理MQ(消息队列)消息时,为了确保数据的正确性,我们通常会先查询数据库中是否存在相关记录,如果存在则更新,否则插入。然而,``当消息数据量非常大时,这种处理方式可能导致消费方法耗时增加,进而引发数据重复入库的问题`` 。
//伪代码示例
public void consumeMessage(Message message) {
String id = message.getId();
Record record = queryRecordById(id);
if (record == null) {
insertRecord(message);
} else {
updateRecord(record, message);
}
}
这里的代码问题就出现在``queryRecordById(id)``,因为数据库事物的隔离性,当存在多个线程并发执行该方法时,``queryRecordById(id)`` 针对同一条数据的2次查询很有可能返回都不存在,进而都执行了``insertRecord(message)``,最终导致数据重复。
特别容易出现在订单,发票创建等场景!
----
案例二:定时任务导致的邮件重复发送
在使用定时任务(如xxl-job)发送邮件时,我们通常会先查询是否存在待发送的邮件,然后执行发送操作,并更新邮件状态为已发送。但如果定时任务的执行频率过快,可能会出现数据库状态尚未更新,而邮件已被再次扫描到的情况,从而导致邮件重复发送。
//伪代码示例
public void sendEmails() {
List<Email> pendingEmails = queryPendingEmails();
for (Email email : pendingEmails) {
sendEmail(email);
updateEmailStatus(email, Sent);
}
}
这段代码看似非常易懂且没有啥大问题,但是如果定时任务调度频率加快或者``sendEmail()``方法存在延时,那么就会导致消息被重复发送。
补充一点: 在使用例如xxl job这类定时任务调度框架时,**一定要注意分析你的任务是否可以被重复调度,以及正确的配置任务的串行配置**,不要误认为调度框架会默认帮你处理。
## 核心思想:幂等性问题
上述两个案例都涉及到了幂等性问题。
> 幂等性是指无论一个操作执行多少次,其结果都是相同的。
在Check-DoSomething问题中,我们需要确保即使在多次执行的情况下,代码的逻辑仍然保持一致。 而这也是能够让我们在任何场景都能避免这类问题的核心能力!!
## 最佳实践
当然,一旦我们发现了问题,那么解决问题的方法就有很多种,下面是一些基本的解决措施。
### 使用悲观锁/乐观锁/Redis锁等机制:通过加锁来确保同一时间只有一个线程能够访问和修改数据。
//使用乐观锁示例
public void updateRecord(Record record, Message message) {
int rowsAffected = recordRepository.updateByOptimisticLock(record, message);
if (rowsAffected == 0) {
throw new ConcurrentModificationException("Record updated concurrently");
}
}
### 使用数据库兜底方案:确保表中具有唯一约束(UK)或主键(PK),并在查询时使用它们来检查数据是否存在。
//使用唯一约束示例
public void insertOrUpdateRecord(Message message) {
try {
recordRepository.insertOrUpdateByUniqueId(message);
} catch (DuplicateKeyException e) {
// Handle duplicate key situation
}
}
### 增加测试范围
在测试中模拟延迟:通过在测试中模拟代码执行延迟、状态更新延迟等场景,来验证代码的幂等性和并发控制能力。
## 结论
Check-DoSomething问题虽然看似简单,但在实际开发中却容易引发各种复杂的问题。通过本文的分析和建议,希望能够帮助你更好地理解和处理这类问题。
在日常编码实践中,我们应该重视幂等性问题的处理,并采取有效的措施来避免潜在的bug。只有这样,我们才能编写出更加健壮、可靠的代码。