大概是我这边在处理队列数据时需要判断表中是否存在,不存在就向表中插入数据,存在的话就更新这条数据,这个逻辑在单进程下正常,但是多进程下,会出现重复入库的问题。
下面代码:以day为条件查询是否存在了当天的统计记录,存在就更新统计,不存在就新增一条当天的统计记录,但是多个进程下,好多个进程取到的都是当天不同时刻的数据,我这边判断只能以日期判断,造成数据重复入库问题。想过在表中加入唯一索引,这样写入时会抛出异常,当前数据重回队列等待下次消费,但是x次后数据就被丢弃了,对这个当日统计记录来说会存在丢数据的风险,求大佬给指点下。
$row = Db::table('statistic')->where('day', $day)->first();
if ($row) {
$data = [
'count' => $row->count + 1,
'cost' => $row->cost + $costTime,
'success_count' => $row->success_count + ($success ? 1 : 0),
'error_count' => $row->error_count + ($success ? 0 : 1),
];
Db::table('statistic')->where('day', $day)->update($data);
} else {
$data = [
'day' => $day,
'count' => 1,
'cost' => $costTime,
'success_count' => $success ? 1 : 0,
'error_count' => $success ? 0 : 1,
];
Db::table('statistic')->insert($data);
}
本问题不在讨论,感觉是弯路,但是下面大佬关于锁的指点确实很有启发,大家看到了只学习大佬的思路,不要看我那个代码逻辑了,误人子弟
不知道咋搞了,我看了下,大概有13个统计表,要是都提前建好,我就不太需要队列处理了,被关锁的话几乎就把多进程强制单进程了,那个加唯一索引 异常处理貌似可以,我测试下
Redis自释放锁:https://www.workerman.net/plugin/55
表中加入了唯一索引,代码调成这种后会出现mysql死锁的情况
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction (SQL: update
project
setupdated_at
= 2023-08-10 12:10:37 whereproject
= 营销系统)我记得我好像在哪看过开发规范,不建议在catch 异常捕获中做业务处理
安装插件:https://www.workerman.net/plugin/55
创建统计锁
使用锁
谢谢大佬 我跑下测试下
大佬 我有十多个表要更新,我实际业务是写了10多个方法,我粘贴出来
有个statistics的统计系统,我抄的这个,但是他的貌似是定时器加redis实现的,我想用队列的多进程
现在是按第一个大佬加表唯一索引写的代码,但是多进程会出现mysql死锁问题,单进程完全没问题
你这种写法数据库的压力有点大;
我简单压测了下,执行时间大部分在0.5s以下,个别在1秒多,电脑4h8g的,确实很耗时,但是这程序只要能正常运行都没问题,前期都不考虑数据库压力了,我担心多进程如果加入了redis锁 会不会更加耗时的问题
原则就是:直接插入的不需要锁;插入或者更新的才需要锁。
按照不同的约束类型,自己拼接锁的key
最好的方法是先内存计算,再批量写数据库。
明白了 大佬 我尝试队列里不在进行数据库操作,只计算业务结果后保留到redis里,然后开启定时器扫描redis批量入库
大佬那个高性能的代码为啥删了啊,我学习下啊,就是那个不读直接更新那里我没看明白,不读的话我怎么确定是更新还是插入,那个goto设计的真好,我这种低端码农都没用过goto
执行流程:
1、标志位的key,自己根据业务组装;
2、不存在标志位时,多进程阻塞等待 获取锁;
3、插入成功后,设置标志位;
4、队列再有数据进来,存在标志位时,多进程同步更新;
谢谢大佬们,受益了
这种多进程处理队列只能有一个干活,其他的都会处于等待中吧
创建,只能有一个;更新是并行的。
看写的人吧。
上述代码经过测试,300条数据,单进程耗时20.487082958221436s,cpu数进程耗时21.118263483047485s,cpu*4进程数耗时22.74993324279785s,对进程处理并没有加快反而由于锁的存在多耗时了;大佬帮看看我上面的代码,我准备放弃了,改用redis+定时器扫描批量入库了
这个结果不稳定,总之很慢,在考虑用定时器执行一次从redis队列取出100条,处理批量入库
盲猜:10张表,你都用StatisticLocker::lock($key, 3, true);加锁,搞成共享锁了。错啦,兄弟
他代码里protected static function assembleData()组装了每个表的key
每张表的key,加索引了吗?没加索引的话,巨慢。
当事人不出来澄清,不好说,也有可能每秒更新很多次,有key也承受不了
确实,最优解还是合并计算、批量插入数据库效率最高。
我写过一个业务,与交易所建立长连接,交易所长连接下发数据很频繁,一条条保存4个进程都积压。搞成批量插入,轻松无压力。
表里测试完 数据会清空,所以目前还没加索引,想着量不大,原来检索数据需要好多条件,我加个key,使用一个了
该来该去,不如单进程效率高
数据库频繁的插入更新可能导致每次锁的释放时间增加;
根据业务逻辑,insert只有一次,update可能存在很多次,这一块是造成数据库压力的原因,所以建议把update部分的内容放到redis队列,其他逻辑保持不变。
那个锁插件的基类代码:
而你用的都是这一个方法,key的取值是否有重复,不得而知。
每一个表的操作是不能共享锁的。也要根据业务的特性,ttl值也是有讲究的。
比如每天、每分钟、每小时才创建一次的,要根据时间合理调整ttl值。
总体原则是:创建时候才加锁,多进程同步更新。
我记得我给你写了很多个方法的啊?为的就是避免锁重复
最优解还是内存计算,批量保存进数据库。
当时,很多方法的时候出现了重复入库的问题,我没研究那个锁的原理 想着是不是方法不一造成的 就改成一个了
算了 我这代码太垃圾了 不值得再探讨了,简单测试数据承载量太低了,这段代码只是实现了业务上的逻辑,实际中根本无法承载业务需求
重复入库是因为数据库压力大,3秒后锁自动释放了。因为是自释放锁,所以可适当延长锁时间比如30秒。
各个数据表,对key做索引就行了。
10张表,就需要10个锁。
我在测试下 大佬 感谢
测试300数据处理 8进程 时间float(20.861277103424072)根之前差不多 这个处理速度太慢了,而且造成数据库压力很大,目前还是没数据的状态,当数据库数据量大的时候,这个处理时间会根漫长,还有key字段加了索引
还是一步到位,改为批量插入吧;
每一个方法名,表示一个锁的前缀,前缀也是锁key的一部分(可看源码);
意思就是前缀不同、key值相同,也是不同的锁。
我们之前访问量不大,直接是使用事务,保证数据的一致性,我问了ai,它让我使用行锁
那个异常用Throwable,感觉你这么写很容易出现死锁的问题,还有我记得事务本身就会加锁啊,你代码里又加了锁
原来是我写的==
mysql 开启事务查询和插入不会加锁,但是更新和删除确实会加锁,where 条件如果有索引会锁行,如果没有索引会锁表
受教了
队列消费不是应该消出库就没了,不存在二次消费吧
不存在,只是我队列里数据有的是重复的 设计更新问题,只是插入的话用队列完全没问题,但是正如上面大佬说的,最高内存计算 批量入库减少数据库压力,也就是仅仅入库的话使用队列并不是完美,队列还是根一些发邮件啊 之类的业务更匹配
你这种情况是多进程并发所致,你的SQL语句并非原子操作。
想要解决这个问题,你只能使用MySQL 自带SQL语句来实现原子操作。
这篇文章能解决这些问题,https://www.ngui.cc/el/1890018.html?action=onClick
建议采用这条SQL写法:ON DUPLICATE KEY UPDATE