今天聊一个老话题,这个在Oracle直接就支持,而对我这种mysql之辈,可能就比较头疼了。老规矩,先来描述一下问题背景:

我们在很多需要解决并发修改db的业务场景中,通常会依赖mysql的Innodb引擎的表来做事务及行级锁。目的就是为了确保并发操作数据问题。在web项目中,几乎任何时候都应该考虑这个问题,毕竟每个来自前端的请求都是并发的被处理(当然你可能在业务代码中做了锁)。考虑到锁的性能问题,有很多高明的设计则是尽可能保证在并发的流程中避免并发数据修改(Netty, kafka)。

web请求这个例子,不是很凸显问题,让我们换个业务场景来继续讨论:假如你电商平台需要实现在用户注册后发送邮件通知,这是一个很常见的需求。一般简单的做法是在db中创建用户条目后,程序调用邮件发送逻辑。处于性能和保证消息高可达的目的,可能会引入消息中间件,来异步处理这个逻辑。但假如程序在向消息中间件投递任务之前宕机了,如何确保邮件一定会投递?这涉及到 分布式事务 ,降低难度后我们也至少要保证任务至少投递一次。

说了这么多,不知道我在讲什么?别着急骂街,原谅我好久没写技术文章,有点生疏。其实我的意思,再不引入复杂的分布式事务实现的情况下,我们可能需要借助db来做数据一致性问题。大概方案就是,将用户信息插入和邮件提醒任务放在一个事务中插入到db(确保任务不丢失),再由一个定时任务(或监听进程)来解决邮件补发消息队列的问题。这听起来似乎并不是什么好方案(挺麻烦)。

下面来说说这个定时任务的逻辑,它需要频繁的去查邮件提醒任务数据表,获取到待投递的任务后将其投递到消息中间件。之前提到了,允许邮件重发,所以在定时任务进程中我们只需要保证数据的最终以执行即可。

接下来是今天的主题,我们需要获取邮件提醒表中的数据时,避免并发操作(假设这个定时任务是多线程,在获取到提醒任务后还要做一系列操作),在获取每一条数据时,都使用select for update来添加一个写锁。每当线程完成消息投递后则删除对应条目,并重新去数据表中获取新的待处理数据。为了避免在获取新数据时由于锁冲突而导致线程阻塞,我们希望能使用nowait特性,但Mysql不支持(好像记得新版本支持了),我们有什么其它方案呢?(终于把预备内容交代完毕了)

在GG上一顿狂搜后,找到了innodb_lock_wait_timeout这个设置项:here

不难理解,只需要如此使用:

1
2
3
4
5
BEGIN;
SET innodb_lock_wait_timeout=1;
SELECT * FROM test WHERE id=1 FOR UPDATE;
....
COMMIT;

这样当多个线程同时试图获取id=1的数据时,除了获得锁的线程外,其它线程都会在阻塞1秒后自动报异常:

1
2
错误代码: 1205
Lock wait timeout exceeded; try restarting transaction

其实mysql默认也是设置了获取锁超时的,只是默认值比较大(50秒),可以通过执行SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout'来获取默认配置。

当然,你也可以在程序中实现超时机制来避免线程阻塞。看你的喜好啦~