起源
之所以想写这个专题,是因为最近在做一个抢占任务的实现。假设数据库很多个任务,在抢占发生之前任务的状态都是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.get
