当前位置: 首页 > 后端技术 > PHP

Laravel多进程数据库队列死锁分析及解决方法

时间:2023-03-29 23:25:58 PHP

问题描述在最近的项目线上环境中,队列服务器上频繁出现数据库死锁问题。这个问题可以追溯到几年前,19年就出现了,那时候经常开发业务功能,所以没有处理过这个问题。这次只是来探究死锁的原因和问题。首先,目前项目中使用的队列驱动是数据库。因为简单、高效、不需要扩展其他第三方应用,所以一直使用mysql数据库作为队列驱动。线上队列环境运行:Ubuntu16.04+Mysql5。7+Laravel5.6,这样的配置目前使用supervisor在上面托管16个队列进程。上图是17,因为有匹配符,所以需要-1,也就是16。查看死锁日志,异常监控这个死锁问题。将近440,000个事件被触发,几乎每分钟都有机会触发死锁。看了队列的源码,发现是X锁导致的,然后尝试模拟更多进程队列消耗是否会导致死锁。多进程消费队列通过artisan命令生成一个测试作业,然后我们暂停每个队列500毫秒,以模拟处理过程。onQueue('test');}配置supervisor托管文件我们使用supervisor来这里托管我们的8个处理进程,配置如下:[program:laravel-worker-queue-test]process_name=%(program_name)s_%(process_num)02dcommand=php/data/sites/test/artisanqueue:work--queue=testautostart=trueautorestart=truenumprocs=8user=rootredirect_stderr=truestdout_logfile=/data/sites/test/storage/logs/worker.log然后启动8个进程,进行测试,发现在消费1400+任务时,发生了456次死锁。下面我们来分析一下死锁的过程,并尝试解决一些解决方案。求职机制(GetJob)我们运行了8个进程,相当于8个工作人员。他们都将执行“求职行动”以获得下一份工作,Laravel源码中的实现是这样的:publicfunctionpop($queue=null){$queue=$this->getQueue($queue);返回$this->database->transaction(function()use($queue){if($job=$this->getNextAvailableJob($queue)){return$this->marshalJob($queue,$job);}returnnull;});}转换成SQL语句如下:BEGINTRANSACTION;SELECT*FROM`jobs`WHERE`queue`=?AND((`reserved_at`ISNULLand`available_at`<=NOW())OR(`reserved_at`<=?))ORDERBY`id`ASClimit1FORUPDATE;UPDATE`jobs`SET`reserved_at`=NOW(),`attempts`=`attempts`+1WHERE`id`=?;COMMIT;第一次select查询主要是获取下一个可用的job,如果available_atdatabase->transaction(function()use($id){if($this->database->table($this->table)->lockForUpdate()->find($id)){$this->database->table($this->table)->where('id',$id)->delete();}});}进入对应的SQL操作:BEGINTRANSACTION;从`jobs`中选择*WHERE`id`=?更新;从`jobs`中删除`id`=?;COMMIT;保存记录,删除它,然后提交整个交易。这是问题开始的地方。通过上面的结构,单个进程执行这个操作应该问题不大,但是当多个进程同时操作执行两套SQL时,就可能出现死锁。当8个进程在同时执行操作时,同时在线频繁操作该表,这里频繁删除、修改、检查该表,可以看成是并发的疯狗操作。问题原因当worker进程(1)在查询下一个可用的worker进程时,他会尝试通过forupdate锁定主键索引(id_index)。工厂进程(2)刚刚处理完一个作业,正在尝试执行删除查询。为了从这个表中删除作业,在可以进行删除的时候,已经获得了主键锁(索引锁),但是删除操作会影响到queue_index,所以查询会请求锁。这可能会造成全局死锁,其中每个事务都在等待另一个事务持有的锁。下面是用脚本模拟整个队列的运行过程,还是产生了大量的死锁:解决方法根据以上问题,想到了一些解决方法,仍然可以有效的处理死锁:1、切换到队列系统到Redis或者Beanstalkd,减少Mysql层面的事务开销,利用内存达到更快的处理速度。2.删除queue_index索引。为了避免死锁,我们可以删除这个条件,但是删除之后,处理速度会大大降低。3、增加软删除:deleted_at,把数据变成更新操作,而不是删除操作。既然是更新,就不会造成死锁(不需要对记录加锁)4、尝试使用第三方扩展包laravel-queue-database-ph4,一个使用S锁实现的数据库队列,增加了一个版本字段来消除死锁问题。