1、需求应用场景是这样的: 使用Oracle数据保存待办任务,使用状态字段区分任务是否已经被执行。多个Worker线程同时执行任务,执行成功或失败后,修改状态字段的值。 假设数据库表结构如下所示。
flag 可取的值包括:0-待办,1-已办,-1-失败待重试。 需要避免的问题: 2、分析2.1、依赖Java语言的机制Java语言的锁机制可以解决并发问题,但只能在单机情况下有效。 在Tomcat(或其他应用服务器)集群环境下,Java代码中的锁机制是解决不了这个问题的。 作为锁的信号量,必须存储在独立于JVM的地方。可以是数据库,可以是Redis。 2.2、Quartz提供的支持在生产环境中,为了避免单点故障,Quartz需要集群提供 HA( High Availability,高可用)支持。Quartz集群依赖将任务信息持久化到数据库中。 有两个可选的思路: 1、可以设置单个节点的worker数量为1。首先保证了在单个节点内不会有并发问题。是否能保证集群中同一个Job只有一个实例在跑,需要考察下 Quartz 提供的文档。 2、在任务类上使用 @DisallowConcurrentExecution 或者 StatefullJob。或许可以达到效果,需要实验。 采用Quartz管理并发问题,采取的是回避策略,不能充分利用计算资源。 Quartz的集群环境依赖JDBC存储,一方面需要通过数据库在节点间共享信息,另一方面,基于数据库的行集锁解决了并发问题。 Quartz 本身已经太庞大,不仔细阅读文档,甚至阅读源代码,也无从猜测其行为,作为企业级的 Timer ,它很好用。解决并发问题,还是找一个更清爽明了的方法吧。 2.3、通过数据库锁实现在考虑解决问题的方案前,先回顾一下数据库的事务隔离级别和Oracle数据库的锁机制。 2.3.1、事务隔离级别事务隔离级别是针对当前会话来说的。 SQL 92标准定义了4种事务隔离级别。 1、Read Uncommited : 可以读到其他会话未提交到。 2、Read Commited :当前会话可以读到其他会话已经提交到数据。
对于当前会话来说,两次读取数据,读到的不一样,这称为“不可重复读”。 这是Oracle默认的事务隔离级别。 3、Repeatable Read :当前会话看不到其他会话已经提交的数据修改,但可以看到其他会话新插入的数据。 不可重复读会出现的问题是:相同的查询条件,在同一个会话中反复执行,查询得到记录条数会不相同。这称为“幻读”。 4、Serializable :其他会话对数据的修改都不可见。 需要注意的是,不是其他会话不能修改数据,而是修改对当前会话不可见。 Oracle支持3种事务隔离级别。 Oracle支持Read Commited、Repeatable Read ,另外,支持 Read Only。 事务隔离级别可以帮我们理解问题,但不是解决问题的方法。 解决问题,靠数据库的锁机制。 2.3.2、Oracle数据库的锁机制我们需要的是DML锁,DML锁的目的在于保证并发情况下的数据完整性。在 Oracle 中,DML锁包括表级锁和行级锁。 select … for update 可以获得行级锁。 这样,我们可以有两个方法达到并发控制的目的: 方法一: 通过select … for update 获得行级锁,锁定若干任务,每条数据是一个任务。 然后执行任务,执行任务完成后,更新状态,提交事务,释放锁。 Quartz 本身是使用这种机制,解决集群中的并发问题的。 相关代码文件包括:
关键方法包括:obainLock,releseLock和executeInLock。 方法二: 按如下步骤执行: 1、执行如下SQL语句,抢占任务。
可以对如上SQL进行改进,只抢占指定数量的任务,多余的任务留给其他 worker 做。 2、逐个执行已经抢占的任务。 可以通过如下SQL查询出已经抢占成功的任务信息。
3、执行完成后,更改任务状态。
如果任务执行失败,放回去。
这种方法的要点在第一步。第一步是会出现并发问题的地方。 Oracle 的update语句会自动获得行级锁。我们可以做如下实验验证:
Oracle 本身对 update 的锁机制已经足以支持我们的工作。 方法三: 增加 version 或 timestamp 字段,使用乐观锁。 有了方法二,这种方法偏麻烦。 |
|