使用MySQL构建一个队列表

使用MySQL来实现队列表是一个取巧的做法,我们看到很多系统在高流量、高并发的情况下表现并不好。典型的模式是一个表包含多种类型的记录:未处理记录、已处理记录、正在处理记录等。一个或者多个消费者线程在表中查找未处理的记录,然后声称正在处理,当处理完成后,再将记录更新成已处理状态。一般的,例如邮件发送、多命令处理、评论修改等会使用类似模式。

通常有两个原因使得大家认为这样的处理方式并不适合。

第一,随着队列表越来越大和索引深度的增加,找到未处理记录的速度会随之变慢。你可以通过将队列表分成两部分来解决这个问题,就是将已处理记录归档或者放到历史表,这可以始终保证队列表很小。

第二,一般的处理过程分两步,先找到未处理记录然后加锁。找到记录会增加服务器的压力,而加锁操作则会让各个消费者进程增加竞争,因为这是一个串行化的操作。
找到未处理记录一般来说都没问题,如果有问题则可以通过使用消息的方式来通知各个消费者。具体的,可以使用一个带有注释的SLEEP()函数做超时处理,如下:

SELECT /* waiting on unsent_emails */ SLEEP(10000);

这让线程一直阻塞,直到两个条件之一满足:10000秒后超时,或者另一个线程使用KILL QUERY结束当前的SLEEP。因此,当再向队列表中新增一批数据后,可以通过SHOW PROCESSLIST,根据注释找到当前正在休眠的线程(我的试验结果是,上面的SELECT的注释并没有显示出来,可以加一个版本号注释:“SELECT /*!999999 waiting on unsent_emails */ SLEEP(10000);”,这样SHOW PROCESSLIST中就可以显示出注释),并将其KILL,然后消费者线程便随之出错后重连继续处理。你可以使用函数GET_LOCK()和RELEASE_LOCK()来实现通知(不建议用这种方式,锁的互斥还异常中断等锁的解除等不太好控制)。或者可以在数据库之外实现,例如使用一个消息服务,进程间通信之类的。

最后需要解决的问题是如何让消费者标记正在处理记录,而不至于让多个消费者重复处理一个记录。我们看到大家一般使用SELECT FOR UPDATE来实现。这通常是扩展性问题的根源,这会导致大量的事务阻塞并等待。
一般,我们要尽量避免使用SELECT FOR UPDATE。不光是队列情况,任何情况下都要尽量避免。总是有别的更好的办法来实现你的目的。在队列表中,可以直接使用UPDATE来更新记录,然后检查是否还有其他的记录需要处理。我们看看具体实现,我们先建立如下的表:

CREATE TABLE unsent_emails (
Id INT NOT NULL PRIMARY KEY AUTO_INCREMETN,
-- columns for the message, from, to subject, etc.
status ENUM(‘unsent’, ‘claimed’, ‘sent’),
owner INT UNSIGNED NOT NULL DEFAULT 0,
ts TIMESTAMP,
KEY (ownet, status, ts)
);

该表的列owner用来存储当前正在处理这个记录的连接线程的线程ID,即由函数CONNECTION_ID()返回的ID。如果当前记录没有被任何消费者处理,则该值为0。

我们还经常看到的一个办法是,如下面所示的一次处理10条记录:

BEGIN;
SELECT id FROM unsent_emails
WHERE owner=0 AND status = ‘unsent’
LIMIT 10 FOR UPDATE;
-- RESULT: 123, 456, 789
UPDATE unsent_emails
SET status = ‘claimed’, owner = CONNECTION_ID()
WHERE id IN(123, 456, 789);
COMMIT;

看到这里的SELECT查询可以用到索引的两个列,因此理论上查找的效率应该更快。问题是,在上面两个查询之间的“间隙时间”,这里的锁会让所有其他同样的查询全部都被阻塞。所有的这样的查询将使用相同的索引,扫描索引相同的部分,所以很可能会被阻塞。

如果改进成下面的写法,则会更高效:

SET AUTOCOMMIT = 1;
COMMIT;
UPDATE unsent_emails
SET status = ‘claimed’, owner = CONNECTION_ID()
WHERE owner = 0 AND status = ‘unsent’
LIMIT 10;
SELECT id FROM unsent_emails
WHERE owner = CONNECTION_ID() AND status = ‘claimed’;
-- result: 123, 456, 789

根本就无须使用SELECT查询去找到哪些记录还没有被处理。客户端的协议会告诉你更新了几条记录,所以可以知道这次需要处理多少条记录。

所有的SELECT FOR UPDATE都可以使用类似的方法改写。

最后还需要处理一种特殊情况:那些正在被进程处理,而进程本身却由于某种原因退出的情况。这种情况处理起来很简单,你只需要定期运行UPDATE语句将它都更新成原始状态就可以了,然后执行SHOW PROCESSLIST,获取当前正在工作的线程ID,并使用一些WHERE条件避免取到那些刚开始处理的进程。假设我们获取的线程ID有(10, 20, 30),下面的更新语句会将处理时间超过10分钟的记录状态都更新成初始状态:

UPDATE unsent_emails
SET owner = 0, status = ‘unsent’
WHERE owner NOT IN (10, 20, 30) AND status = ‘claimed’
AND ts < CURRENT_TIMESTAMP – INTERVAL 10 MINUTE;

另外,注意看看是如何巧妙地设计索引让这个查询更加高效的。因为我们将范围条件放在WHERE条件的末尾,这个查询恰好能够用到索引的全部列。其他的查询也都能用上这个索引,这就避免了再新增一个额外的索引来满足其他的查询。

我们总结一下这个队列表实现中的一些基本原则:
尽量少做事,可能的话就不要做任何事情。除非不得已,否则不要使用轮询,因为这会增加负载,而且还会带来很多低产的工作。
尽可能快地完成需要做的事情。尽量使用UPDATE代替先SELECT FOR UPDATE再UPDATE的写法,因为事务提交的越快,持有的锁时间就越短,可以大大减少竞争和加速串行执行效率。
将已处理完成和未处理的数据分开,保证数据集足够小。
某些查询是无法优化的,考虑使用不同的查询或者不同的策略去实现相同的目的。通常对于SELECT FOR UPDATE就需要这样的处理。

有时,最好的办法就是将任务队列从数据库中迁移出来。Redis就是一个很好的容器,也可以使用memcached来实现。另一个选择是使用Q4M存储引擎,但我们没有在生产环境使用过这个存储引擎。RabbitMQ和Gearman也可以实现类似的功能。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,我明白了。关于你的问题,MySQL适合存储结构化数据,比如用户信息、订单、文章等等。而Redis适合存储非结构化数据,比如缓存数据、消息列、分布式锁等等。此外,由于Redis拥有非常高的读写性能,可以用于构建高并发的缓存系统。希望这个回答能对你有帮助。 ### 回答2: 在选择使用MySQL还是Redis的情况下,需考虑到两者的特点及应用场景。 MySQL是一个关系型数据库管理系统,适用于处理结构化数据。它适用于数据之间有复杂关系、需要备份和恢复、涉及事务处理、需要强一致性的场景。MySQL可以存储大量数据,并支持复杂的查询和聚合操作,适用于数据量较大且需要进行复杂查询的应用。例如,电子商务平台通常需要处理大量用户和订单数据,使用MySQL更适合。 Redis是一个高性能的内存数据库,适用于处理非结构化或半结构化数据,并提供了丰富的数据结构,如字符串、哈希表、列表、集合和有序集合。Redis具有快速的读写速度和高并发性能,并且可以将部分数据持久化到硬盘上。适用于对数据访问速度要求较高的应用,如缓存、会话管理、排行榜和实时分析。例如,社交媒体应用通常需要快速读取用户关注和推荐用户,使用Redis更合适。 综上所述,使用MySQL适合于需要复杂查询、事务处理和强一致性的应用场景,而Redis适用于高性能、快速读写和对数据结构有特殊要求的应用场景。在实际应用中,可以结合两者的优势进行合理选择,如使用MySQL存储主要数据,将常用数据放入Redis缓存,以提高应用的整体性能和响应速度。 ### 回答3: MySQL和Redis是两种常用的数据库管理系统,它们适用于不同的场景和需求。 MySQL适用于以下情况: 1. 数据的一致性和持久性要求较高的场景,如金融系统、电子商务平台等需要严格的数据管理和保证的领域。 2. 需要复杂的查询和关系型数据操作的场景,如需要进行多表连接、嵌套查询、事务管理等的应用。 3. 数据量较大的场景,MySQL具有较好的扩展性和稳定性,能够应对大规模数据的存储和访问。 4. 需要使用SQL语言进行数据操作和管理的场景,MySQL是一种成熟的关系型数据库系统,提供了广泛的SQL支持。 Redis适用于以下情况: 1. 需要高性能的场景,如高并发访问、实时推送、缓存等应用,Redis具有快速读写速度和高并发处理能力。 2. 需要对数据进行快速操作和存储的场景,Redis将数据全部存储在内存中,读写速度远高于磁盘IO的数据库系统。 3. 需要对数据进行简单的操作和处理的场景,Redis不支持复杂的查询和事务操作,适合用于简单的键值对存储和操作。 4. 需要对大数据量进行计数、排行等操作的场景,Redis提供了丰富的数据结构和功能,如set、list、hash等,方便进行统计和计算。 综上所述,在选择数据库管理系统时,需根据实际的需求和场景来进行选择。如果需要复杂的查询和事务管理,以及对数据的持久性和一致性要求较高,则选择MySQL;如果对性能和简单的数据操作有较高的要求,或需要进行缓存和计算等操作,则选择Redis。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值