For update or not

起源

​ 之所以想写这个专题,是因为最近在做一个抢占任务的实现。假设数据库很多个任务,在抢占发生之前任务的状态都是FREE。现在假设同时有一堆抢占线程开始工作,抢占线程会查找数据库中状态为FREE的任务,并且将其状态置为BUSY,然后开始执行对应任务。执行完成之后,再将任务状态置为FINISH。任何任务都是不能被重复执行的,即必须保证所有任务都只能被一个线程执行。

​ 笔者和人民群众一样,第一个想到的就是利用数据库的for update实现悲观锁。这样肯定能够保证数据的强一致性,但是这样会大大影响效率,加重数据库的负担。想到之前看过的一篇文章https://www.cnblogs.com/bigben0123/p/8986507.html,文章里面有提到数据库引擎本身对更新的记录会行级上锁。这个行级锁的粒度非常细,上锁的时间窗口也最少,只有在更新记录的那一刻,才会对记录上锁。同时笔者也想到在前一家公司工作的时候,当时有幸进入到了核心支付组,负责过一段时间的账务系统。当时使用的是mysql的InnoDB引擎。记得当时的代码在往账户里面加钱的时候是没有加任何锁的,只有在从账户扣钱的时候才用for update。所以这个问题应该有更加完美的答案......


探索之路

for update的实现这里就不再做过多尝试了。这里笔者直接探索在没有for update的时候高并发情况下是否会有问题。具体尝试的过程如下:

造测试数据

​ 首先建立一个任务表,为了简单模拟,我们这里就只添加必要的字段。建表语句如下:

create table task(
ID NUMBER(10) NOT NULL,
TASK_RUN_STATUS NUMBER(4) NOT NULL
);
comment on table task is '互斥任务表';
comment on column task.ID is '主键ID.';
comment on column task.TASK_RUN_STATUS is '任务运行状态(1.初始待运行 2.运行中 3.运行完成).';
alter table task add constraint TASK_PK primary key (ID) using index;

​ 为了方便测试,这里我们加入三条任务记录,插入任务记录的语句如下:

insert into task(id, task_run_status) values(0, 1);
insert into task(id, task_run_status) values(1, 1);
insert into task(id, task_run_status) values(2, 1);

模拟并发抢占

public class MultiThreadUpdate {
public static void main(String[] args) throws Exception {
Class.forName("oracle.jdbc.OracleDriver");
ExecutorService executorService = Executors.newFixedThreadPool(30);
List<Future<Void>> futures = new ArrayList<Future<Void>>(); // 每个ID开20个线程去并发更新数据
for (int i=0; i<20; i++) {
for (int j=0; j<3; j++) {
final int id = j;
futures.add(executorService.submit(new Callable<Void>() {
public Void call() throws Exception {
Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "czbank", "123456");
// con.setAutoCommit(false); // 不自动提交事务
PreparedStatement pstm = con.prepareStatement("update task set TASK_RUN_STATUS = ? where id = ? and TASK_RUN_STATUS = ?");
pstm.setInt(1, 2);
pstm.setInt(2, id);
pstm.setInt(3, 1);
int upRec = pstm.executeUpdate();
// 打印更新的记录条数
System.out.println("Thread:" + Thread.currentThread().getName() + " updated(id=" + id + "):" + upRec + " records...");
// Thread.sleep(1000); // 在事务提交之前,其线程都会阻塞直到对特定记录的更新提交
// con.commit();
con.close();
pstm.close();
return null;
}
}));
}
}
executorService.shutdown();
}
}

​ 最终程序的输出结果如下:

Thread:pool-1-thread-9 updated(id=2):0 records...
Thread:pool-1-thread-15 updated(id=2):0 records...
Thread:pool-1-thread-22 updated(id=0):0 records...
Thread:pool-1-thread-28 updated(id=0):0 records...
Thread:pool-1-thread-14 updated(id=1):0 records...
Thread:pool-1-thread-17 updated(id=1):0 records...
Thread:pool-1-thread-26 updated(id=1):0 records...
Thread:pool-1-thread-30 updated(id=2):0 records...
Thread:pool-1-thread-29 updated(id=1):0 records...
Thread:pool-1-thread-27 updated(id=2):0 records...
Thread:pool-1-thread-5 updated(id=1):0 records...
Thread:pool-1-thread-23 updated(id=1):0 records...
Thread:pool-1-thread-21 updated(id=2):1 records...
Thread:pool-1-thread-1 updated(id=0):1 records...
Thread:pool-1-thread-6 updated(id=2):0 records...
Thread:pool-1-thread-8 updated(id=1):1 records...
Thread:pool-1-thread-10 updated(id=0):0 records...
Thread:pool-1-thread-13 updated(id=0):0 records...
Thread:pool-1-thread-4 updated(id=0):0 records...
Thread:pool-1-thread-19 updated(id=0):0 records...
Thread:pool-1-thread-16 updated(id=0):0 records...
Thread:pool-1-thread-2 updated(id=1):0 records...
Thread:pool-1-thread-11 updated(id=1):0 records...
Thread:pool-1-thread-7 updated(id=0):0 records...
Thread:pool-1-thread-25 updated(id=0):0 records...
Thread:pool-1-thread-3 updated(id=2):0 records...
Thread:pool-1-thread-18 updated(id=2):0 records...
Thread:pool-1-thread-12 updated(id=2):0 records...
Thread:pool-1-thread-20 updated(id=1):0 records...
Thread:pool-1-thread-24 updated(id=2):0 records...
Thread:pool-1-thread-15 updated(id=2):0 records...
Thread:pool-1-thread-9 updated(id=0):0 records...
Thread:pool-1-thread-22 updated(id=1):0 records...
Thread:pool-1-thread-30 updated(id=0):0 records...
Thread:pool-1-thread-5 updated(id=1):0 records...
Thread:pool-1-thread-17 updated(id=2):0 records...
Thread:pool-1-thread-26 updated(id=0):0 records...
Thread:pool-1-thread-29 updated(id=1):0 records...
Thread:pool-1-thread-27 updated(id=2):0 records...
Thread:pool-1-thread-28 updated(id=0):0 records...
Thread:pool-1-thread-21 updated(id=1):0 records...
Thread:pool-1-thread-1 updated(id=2):0 records...
Thread:pool-1-thread-14 updated(id=0):0 records...
Thread:pool-1-thread-2 updated(id=1):0 records...
Thread:pool-1-thread-16 updated(id=0):0 records...
Thread:pool-1-thread-4 updated(id=2):0 records...
Thread:pool-1-thread-13 updated(id=1):0 records...
Thread:pool-1-thread-19 updated(id=2):0 records...
Thread:pool-1-thread-6 updated(id=0):0 records...
Thread:pool-1-thread-8 updated(id=1):0 records...
Thread:pool-1-thread-10 updated(id=2):0 records...
Thread:pool-1-thread-23 updated(id=0):0 records...
Thread:pool-1-thread-11 updated(id=1):0 records...
Thread:pool-1-thread-7 updated(id=2):0 records...
Thread:pool-1-thread-25 updated(id=0):0 records...
Thread:pool-1-thread-3 updated(id=1):0 records...
Thread:pool-1-thread-18 updated(id=2):0 records...
Thread:pool-1-thread-12 updated(id=0):0 records...
Thread:pool-1-thread-20 updated(id=1):0 records...
Thread:pool-1-thread-24 updated(id=2):0 records...

​ 可以看到,即使在没有显示使用事务的情况下,多线程并发执行也能够保证某一条数据的更新只被执行一次。


最终任务设计

​ 通过上面的测试例子,已经验证了我的猜想。接下来就是如何设计抢占任务的执行步骤了。废话不多说,直接上基本代码:

public void runMutexTasks(MutexTaskDto runCond) throws Exception {
// STEP1: 先去查找待执行的互斥任务
runCond.setTaskRunStatus(Enums.MutexTaskRunStatus.WAIT_RUN.getKey()); // 待运行
runCond.setPhysicsFlag(Enums.TaskStatus.NORMAL.getKey()); // 正常状态(未废弃)
PageInfo<MutexTaskDto> runnableTasks = MutexTaskService.pagingQueryGroupByTaskId(0, 0, runCond);
if (CollectionUtils.isEmpty(runnableTasks.getRows())) {
LOGGER.debug("根据条件未找到待执行的互斥任务,跳过执行......");
return;
} // STEP2: 分别尝试执行
List<MutexTaskDto> runTasks = null;
Collections.shuffle(runnableTasks.getRows()); // 打乱顺序
for (MutexTaskDto oneTask : runnableTasks.getRows()) {
runTasks = mutexTaskService.selectRunnableTaskByTaskId(oneTask.getTaskId());
if (CollectionUtils.isEmpty(runTasks)) {
LOGGER.info("互斥任务ID【{}】已不是待运行状态,跳过任务执行......", oneTask.getTaskId());
continue;
} // STEP3: 运行任务
MutexTaskDto updateCond = new MutexTaskDto();
updateCond.setTaskRunStatus(Enums.MutexTaskRunStatus.RUN_SUCCESS.getKey());
updateCond.setTaskPreStatus(Enums.MutexTaskRunStatus.RUNNING.getKey());
updateCond.setTaskId(oneTask.getTaskId());
try {
runTasks(runTasks);
} catch(Exception e) {
updateCond.setRunRemark(getErrorMsg(e));
updateCond.setTaskRunStatus(Enums.MutexTaskRunStatus.RUN_FAILED.getKey());
mutexTaskService.updateByTaskId(updateCond);
// 这里只打印失败结果,具体失败信息需要上层调用方法日志打印出来
LOGGER.error("互斥任务ID【{}】执行失败!", oneTask.getTaskId());
throw e;
}
mutexTaskService.updateByTaskId(updateCond);
LOGGER.info("互斥任务ID【{}】执行成功......", oneTask.getTaskId());
Thread.sleep(1000); // 抢到了一个节点执行权限,此处暂停1s,给其他机器机会
}
} // 其中mutexTaskService的selectRunnableTaskByTaskId方法如下:
// 不使用事务,利用数据库引擎自身的行级锁 public List<MutexTaskDto> selectRunnableTaskByTaskId(String taskId) {
// STEP1: 先用查询数据(一个taskID可能对应多条记录,对应不同的参数)
List<MutexTaskModle> mutexTaskModles = this.mutexTaskDao
.selectByTaskId(taskId);
if (CollectionUtils.isEmpty(mutexTaskModles)) {
return Collections.emptyList();
} // STEP2: 更新数据(使用数据库引擎自身所带的行级锁)
MutexTaskModle updateInfo = new MutexTaskModle();
updateInfo.setTaskRunStatus(2);
updateInfo.setTaskPreStatus(1);
updateInfo.setTaskId(taskId);
int updateCount = cleaningMutexTaskDao.updateByTaskId(updateInfo);
if (updateCount <= 0) {
LOGGER.info("找到待执行的互斥任务,但是更新任务为执行中失败......");
return Collections.emptyList();
} // STEP3: 前面两项都校验过,则确认当前任务列表是可以执行的
List<MutexTaskDto> mutexTasks = BeanConvertUtils.convertList(mutexTaskModles,
MutexTaskDto.class);
return mutexTasks;
}

​ 关键点就在于第58行的cleaningMutexTaskDao.updateByTaskId(updateInfo);。该语句对应的SQL大致为:

update TASK set task_status = ? where task_id = ? and task_tatus = ?

​ 其中task_id为表的主键,且启用了唯一索引。


总结

​ 这个问题刚开始笔者想到的解决方案就是使用for update。但内心总觉得这不是最佳方案,想起以前做过的项目还有看过的文章,却也总是不太确定。最终还是自己动手写了个测试用例"释怀"了内心的疑惑。最终也顺利地想出了这个"完美"的实现。不得不承认:实践是检验真理的唯一标准!工作到现在,越来越觉得大家觉得最好的实现不一定就是最好的,大家认为的最高效的方法不一定就是最高效的。很多事情没有绝对,就像写代码一样,没有绝对的好代码。

​ 当然这不是鼓励大家随便写代码,笔者想说的是:做软件就像做学问。不能纯粹地拿别人的结论奉为圣经。遇到问题要多思考,才会有自己的沉淀。思考之后要多行动,才不会仅仅停留在思想的巨人,行动的矮子。当然,行动之后也要多多整理出来,就像笔者这样,奉献社会,方便你我他......(一脸无语)

For update带来的思考的更多相关文章

  1. 一次flume exec source采集日志到kafka因为单条日志数据非常大同步失败的踩坑带来的思考

    本次遇到的问题描述,日志采集同步时,当单条日志(日志文件中一行日志)超过2M大小,数据无法采集同步到kafka,分析后,共踩到如下几个坑.1.flume采集时,通过shell+EXEC(tail -F ...

  2. 用Vue自己造个组件轮子,以及实践背后带来的思考

    前言 首先,向大家说声抱歉.由于之前的井底之蛙,误认为Vue.js还远没有覆盖到二三线城市的互联网小厂里.现在我错了,从我司的前端技术选型之路便可见端倪.以太原为例,已经有不少公司陆续开始采用Vue. ...

  3. 第6届蓝桥杯javaA组第7题,牌型种数,一道简单的题带来的思考

    题目: 小明被劫持到X赌城,被迫与其他3人玩牌. 一副扑克牌(去掉大小王牌,共52张),均匀发给4个人,每个人13张. 这时,小明脑子里突然冒出一个问题: 如果不考虑花色,只考虑点数,也不考虑自己得到 ...

  4. 原生javascript难点总结(1)---面向对象分析以及带来的思考

    ------*本文默认读者已有面向对象语言(OOP)的基础*------ 我们都知道在面向对象语言有三个基本特征 :  封装 ,继承 ,多态.而js初学者一般会觉得js同其他类C语言一样,有类似于Cl ...

  5. 微信小程序开发带来的思考

    若无小程序开发经验,可先阅读 玩转微信小程序 一文. 微信小程序正式上线已有几周时间,相信它的开发模式你已烂熟于胸,可能你也有所疑问,我竟能用 web 语言开发出如此流畅的几乎原生体验的应用.可能你又 ...

  6. 从壹开始前后端分离 [.netCore 填坑 ] 三十四║Swagger:API多版本控制,带来的思考

    前言 大家周二好呀,.net core + Vue 这一系列基本就到这里差不多了,今天我又把整个系列的文章下边的全部评论看了一下(我是不是很负责哈哈),提到的问题基本都解决了,还有一些问题,已经在QQ ...

  7. Gevent 性能和 gevent.loop 的运用和带来的思考

    知乎自己在底层造了非常多的轮子,而且也在服务器部署方面和数据获取方面广泛使用 gevent 来提高并发获取数据的能力.现在开始我将结合实际使用与测试慢慢完善自己对 gevent 更全面的使用和扫盲. ...

  8. mysql for update 高并发 死锁研究

    mysql for update语句     https://www.cnblogs.com/jtlgb/p/8359266.html For update带来的思考 http://www.cnblo ...

  9. 64位进程调用32位dll的解决方法 / 程序64位化带来的问题和思考

    最近做在Windows XP X64,VS2005环境下做32位程序编译为64位程序的工作,遇到了一些64位编程中可能遇到的问题:如内联汇编(解决方法改为C/C++代码),long类型的变化,最关键的 ...

随机推荐

  1. Mongodb 与 SQL 语句对照表

    In addition to the charts that follow, you might want to consider the Frequently Asked Questions sec ...

  2. 射线和三角形的相交检测(ray triangle intersection test)【转】

    本文以Fast, Minimum Storage Ray Triangle Intersection为参考,在此感谢原作者,大家也可以直接阅读原版. 概述 射线和三角形的相交检测是游戏程序设计中一个常 ...

  3. udid iphone6 获取

    http://www.udidregistration.org/how-to-find-udid-of-iphone-6.html

  4. apache测试网页执行效率

    apache软件下有一个测试网页访问速度的工具ab.exe,位于apache的bin目录下,windows下使用命令行进入bin目录,执行ab.exe -n 10000 -c 10 http://12 ...

  5. FunDA(8)- Static Source:保证资源使用安全 - Resource Safety

    我们在前面用了许多章节来讨论如何把数据从后台数据库中搬到内存,然后进行逐行操作运算.我们选定的解决方案是把后台数据转换成内存中的数据流.无论在打开数据库表或从数据库读取数据等环节都涉及到对数据库表这项 ...

  6. Xcode10 libstdc++.6.0.9.tbd移除引起的错误

    /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/u ...

  7. (转)ReentrantLock可重入锁的使用场景

    原文: http://my.oschina.net/noahxiao/blog/101558

  8. 实现可搜索仿select下拉选中

    由于在优化项目中,发现先前写的一个活化石级的的可搜索下拉功能在高速搜索中会出现卡顿现象 1.起初的解决方法是在搜索事件中加入防抖函数隔一段时间才去触发他,同时搜索的不再是html文档片段,而是直接对数 ...

  9. Code First 数据迁移 转

    一.为模型更改设置 Code First 数据迁移 1.工具—>库程序包管理器—>程序包管理器控制台—>输入“Enable-Migrations”  或者 Enable-Migrat ...

  10. POJ 2377

    #include<stdio.h> #define MAXN 1005 #include<iostream> #include<algorithm> #define ...