Nginx重试机制导致数据库出现重复记录

本文深入探讨了Nginx反向代理下,因重试机制导致的同一HTTP请求被执行两次的问题,特别是在数据库插入操作时,如何通过调整配置避免数据冗余。

前言

  • 前几天正在升级发版项目的时候,发现一个很奇葩的问题,就是用户再浏览器里执行了一次http请求,结果后端服务器执行了两遍,如果这次请求是Insert操作,可想而知,数据库会多出一条一模一样的记录来。

场景还原

  • 网关用Nginx做了反向代理和负载均衡,Nginx下挂着两台阿里云ECS服务器,每台机器上都装着Tomcat,用户打开浏览器,点击页面,访问后端接口,查看Nginxaccess.log,结果这一条请求打在了两台服务器上。

问题剖析

nginx的重试机制就是容错的一种,在nginx的配置文件中,proxy_next_upstream项定义了什么情况下进行重试,官网文档中给出的说明如下:

Syntax:	proxy_next_upstream error | timeout | invalid_header | http_500 | http_502 | http_503 | http_504 | http_403 | http_404 | off 
Default:	proxy_next_upstream error timeout;
Context:	http, server, location
  • 默认情况下,当请求服务器发生错误或超时时,会尝试到下一台服务器。

  • 问题找到了,原因是Nginx配置文件中,超时时间太短了:proxy_connect_timeout 20;;在Nginx的默认配置是:在客户端请求服务器超时的情况下,Nginx会自动转发该请求到另外一台服务器上,这是Nginx的一种容错机制,所以Nginx的访问日志中会出现同一条请求而两台服务器都执行了一遍的情况,这样以来,程序如果没有做幂等性操作的话数据库会出现两条记录。

  • 还有一个参数影响了重试的次数:proxy_next_upstream_tries,官方文档中给出的说明如下:

    Syntax:	proxy_next_upstream_tries number;
    Default:	proxy_next_upstream_tries 0;
    Context:	http, server, location
    This directive appeared in version 1.7.5.

问题解决

本来就是Nginx的一种容错机制,这种机制在查询操作还是挺好的,如果是插入操作,那就有点问题了,如果这条插入的请求特别耗时,并且时间超过Nginx的proxy_connect_timeout时间设置,Nginx会自动将该请求转发集群中的另外一台服务器的。但是我们不能将这种机制关闭,关闭以后会影响Nginx效率的,那怎么办哪?于是想出了一个临时解决方案,专门针对耗时时间长的几个接口做一下过滤,也就是说,在Nginx的server配置标签中,专门对几个特定的url过过滤,关闭Nginx的重试机制,配置如下

server {
        
		
       location ~ /api/insertData {
              proxy_connect_timeout 60;
              proxy_send_timeout 60;
              proxy_read_timeout 60;
              proxy_next_upstream off;
        
        }
 }

小结

将重试机制关闭就可以了,主要是这个配置:proxy_next_upstream off;,好了,问题解决!

public function ReverseDeliveryFeeVerifyDo($yearMonth): bool { $monthTimeRange = $this->getMonthTimeRange($yearMonth); // 一次性查询出逆向配送费报价表数据 避免重复查询数据库 $quoteList = QuoteModel::field('warehous,price')->select()->toArray(); $quoteMap = []; if (!empty($quoteList)) { $quoteMap = array_column($quoteList, 'price', 'warehous'); } else { //Log::warning('【逆向配送费核对仓库名称不匹配】--月份为:' . $yearMonth . "的费用核对匹配不到逆向配送费报价表的仓库名称"); return false; } $status = true; // 初始化状态 $itemizationModel = new ItemizationModel(); $batchCount = 0; // 批次计数器 $reconnectInterval = 100; // 每100批重连一次 try { // 使用chunk方法分批处理数据,每批处理50条 ItemizationModel::whereBetween('business_time', [ $monthTimeRange['startTime'], $monthTimeRange['endTime'] ])->field('id,billing_amount,warehouse_name,theoretical_amount,fee_difference')->chunk(10, function ($items) use ($yearMonth, $quoteMap, &$status, $itemizationModel, &$batchCount, $reconnectInterval) { // 批次计数递增 $batchCount++; // 达到重连间隔时,重新连接数据库 if ($batchCount % $reconnectInterval === 0) { $this->reconnectDatabase(); //Log::info("【数据库重连】已处理{$batchCount}批,重新连接数据库"); } $batchData = []; foreach ($items as $item) { $itemData = $item->toArray(); // 转换为数组便于处理 if (!empty($itemData['warehouse_name']) && isset($quoteMap[$itemData['warehouse_name']])) { $itemData['theoretical_amount'] = $quoteMap[$itemData['warehouse_name']]; $itemData['fee_difference'] = $itemData['billing_amount'] - $quoteMap[$itemData['warehouse_name']]; } else { $itemData['theoretical_amount'] = ''; $itemData['fee_difference'] = ''; //Log::warning('【逆向配送费核对仓库名称不匹配】--月份为:' . $yearMonth . "的费用核对匹配不到逆向配送费报价表的仓库名称,明细数据id为" . $itemData['id']); } $batchData[] = $itemData; unset($item, $itemData); // 释放单条数据内存 } // 使用模型批量更新 if (!empty($batchData)) { $itemizationModel->saveAll($batchData); } // 释放批次数据内存并触发垃圾回收 unset($items, $batchData); gc_collect_cycles(); }); } catch (\Exception $e) { print_r("报错了直接干死整个进程,具体报错信息如下:".$e->getMessage());die; //Log::error('【逆向配送费核对异常】--月份为' . $yearMonth . "的费用核对发生错误:" . $e->getMessage()); $status = false; } // 释放主函数中的大型变量 unset($quoteList, $quoteMap, $itemizationModel); gc_collect_cycles(); return $status; } /** * 数据库重连方法 */ private function reconnectDatabase() { try { $connection = \think\facade\Db::connect(); $connection->close(); $connection->connect(); } catch (\Exception $e) { print_r("数据库连接失败:".$e->getMessage());die; Log::error('【数据库重连失败】' . $e->getMessage()); } } | 我利用上面的代码读取mysql的数据然后进行逻辑运算然后再更新回去 100万的数据到了80万左右就会报错:SQLSTATE[HY000]: General error: 2006 MySQL server has gone away 错误 我的mysql的配置如下: [mysqld] #服务Id唯一 server-id = 1 port = 3306 log-error = /var/log/mysql/error.log # 只能用IP地址 skip_name_resolve # 数据库默认字符集 character-set-server = utf8 # 数据库字符集对应一些排序等规则 collation-server = utf8_general_ci # 设置client连接mysql时的字符集,防止乱码 init_connect='SET NAMES utf8' # 最大连接数 max_connections = 10000 # 增加连接错误上限 max_connect_errors = 10000 # 服务器等待客户端请求的超时时间(秒),默认8小时 wait_timeout = 28800 # 交互式连接超时时间(秒),默认28800 interactive_timeout = 28800 # 关闭前等待活动的时间(秒),默认28800 net_read_timeout = 28800 net_write_timeout = 28800 我的php.ini里设置如下: post_max_size = 100M upload_max_filesize = 50M memory_limit = 1024M 我的nginx里面的设置如下: client_max_body_size 100M; client_body_buffer_size 1024k; proxy_connect_timeout 3600; # 与上游服务器的连接超时 proxy_read_timeout 3600; # 从上游服务器读取响应的超时 proxy_send_timeout 3600; # 发送请求到上游服务器的超时 fastcgi_connect_timeout 3600; fastcgi_read_timeout 3600; fastcgi_send_timeout 3600; 请帮我分析一下这到底是什么问题导致的呢?
最新发布
07-06
<think>根据提供的代码和错误信息,错误是"MySQL server has gone away",这通常是由于MySQL连接超时或数据包过大导致的。以下是一些可能的原因和解决方案: ### 可能的原因分析: 1. **MySQL连接超时**:MySQL服务器在空闲一段时间后会关闭连接。默认的`wait_timeout`是8小时(28800秒),但在处理大量数据时,可能因为每个批次处理时间较长,导致连接在批次之间空闲时间超过MySQL的`wait_timeout`设置。然而,从代码中看,每100批(每批10条,即1000条)会重连一次,这个频率可能不足以防止超时,因为处理1000条的时间可能超过了`wait_timeout`(但一般不会,除非单条处理非常慢)。但注意,`wait_timeout`是空闲超时,如果处理时间很长,但连接一直在使用,则不会超时。所以这里更可能是其他原因。 2. **数据包过大**:当更新大量数据时,如果一次更新的数据量过大(比如批量更新的数据包超过`max_allowed_packet`设置),会导致MySQL断开连接。在代码中,每批处理10条记录,然后进行批量更新。10条记录的数据包应该不会太大,但也要注意每条记录的数据量。另外,在重连间隔(100批,即1000条)时重连,但批量更新是每10条一次,所以每次批量更新的数据包应该不会太大。 3. **服务器资源问题**:处理大量数据时,可能会消耗大量内存,导致PHP或MySQL内存不足。 4. **网络问题**:不稳定的网络连接可能导致连接断开。 5. **MySQL服务器配置**:检查`max_allowed_packet`设置,如果批量更新的数据包超过这个值,就会导致错误。 ### 解决方案: #### 1. 调整MySQL配置: - 增加`max_allowed_packet`:在MySQL配置文件(my.cnf或my.ini)中,设置`max_allowed_packet=256M`(或者更大,根据实际需要),然后重启MySQL。 - 调整超时设置:虽然已经设置了较大的超时时间,但可以尝试进一步增加`wait_timeout`和`interactive_timeout`,但这可能不是主要原因。 #### 2. 优化代码中的重连机制: - 当前代码每100批(1000条)重连一次。如果处理1000条的时间超过了MySQL的`wait_timeout`(8小时显然不会),那么可能是其他原因。但为了确保连接活跃,可以在每次批量更新后立即释放连接,然后在下一次获取数据前重连。或者,可以尝试减少重连间隔,比如每50批重连一次。 #### 3. 减少批量更新的数据量: - 虽然当前每批只有10条,但如果每条记录的数据量很大,也可能导致数据包超过`max_allowed_packet`。可以尝试减少每批处理的条数,比如5条。 #### 4. 检查并优化内存使用: - 代码中已经有一些内存释放和垃圾回收的操作。但可以进一步优化,比如在循环内部及时释放变量。 - 另外,确保PHP的`memory_limit`足够大(当前是1024M,对于100万条数据处理可能不够,因为每次批量更新10条,但整个chunk循环会累计占用内存吗?注意chunk方法内部是分批次获取的,所以不会一次性加载所有数据)。但是,在循环内部,我们使用`$batchData`数组保存了10条数据,然后批量更新。这个数组占用的内存应该不大。 #### 5. 使用更高效的数据处理方式: - 考虑使用游标(cursor)来逐条处理,但这样可能会慢,但更节省内存。 - 或者,使用分页查询而不是chunk,因为chunk可能会在内部使用相同的连接,而分页可以每次使用新的连接。 #### 6. 增加错误重试机制: - 在捕获到“MySQL server has gone away”错误时,尝试重新连接并重试当前批次。 ### 修改代码建议: 在现有代码中,我们已经有了重连机制,但可能不够。我们可以尝试以下修改: 1. **在每次批量更新前都重连数据库**(但这样性能开销大,不推荐)。或者,在每次chunk回调开始时重连(即每次处理10条记录前都重连)?这显然不可取。 2. **调整重连间隔**:将重连间隔调整为更小的值,比如每10批(100条)重连一次。这样虽然重连次数增多,但可以避免连接超时。 3. **在批量更新后立即释放连接**:在ThinkPHP中,可以手动断开连接,然后在下一个批次前重新连接。但这样可能会影响性能。 4. **检查max_allowed_packet**:首先确定是不是这个原因。可以在MySQL中执行`SHOW VARIABLES LIKE 'max_allowed_packet';`查看当前值。如果值较小(比如默认1M),则需要增大。 ### 修改后的代码示例: 我们调整重连间隔,比如每50批(500条)重连一次。同时,我们增加对`max_allowed_packet`的配置调整(需要在MySQL服务器上调整)。 另外,我们可以在捕获到异常时,如果是“MySQL server has gone away”,则尝试重连并重试当前批次(这里暂不实现,因为需要判断异常信息)。 修改重连间隔为50: ```php public function ReverseDeliveryFeeVerifyDo($yearMonth): bool { $monthTimeRange = $this->getMonthTimeRange($yearMonth); // 一次性查询出逆向配送费报价表数据 避免重复查询数据库 $quoteList = QuoteModel::field('warehous,price')->select()->toArray(); $quoteMap = []; if (!empty($quoteList)) { $quoteMap = array_column($quoteList, 'price', 'warehous'); } else { //Log::warning('【逆向配送费核对仓库名称不匹配】--月份为:' . $yearMonth . "的费用核对匹配不到逆向配送费报价表的仓库名称"); return false; } $status = true; // 初始化状态 $itemizationModel = new ItemizationModel(); $batchCount = 0; // 批次计数器 $reconnectInterval = 50; // 修改为每50批重连一次(原来是100) try { // 使用chunk方法分批处理数据,每批处理10条 ItemizationModel::whereBetween('business_time', [ $monthTimeRange['startTime'], $monthTimeRange['endTime'] ])->field('id,billing_amount,warehouse_name,theoretical_amount,fee_difference')->chunk(10, function ($items) use ($yearMonth, $quoteMap, &$status, $itemizationModel, &$batchCount, $reconnectInterval) { // 批次计数递增 $batchCount++; // 达到重连间隔时,重新连接数据库 if ($batchCount % $reconnectInterval === 0) { $this->reconnectDatabase(); //Log::info("【数据库重连】已处理{$batchCount}批,重新连接数据库"); } $batchData = []; foreach ($items as $item) { $itemData = $item->toArray(); // 转换为数组便于处理 if (!empty($itemData['warehouse_name']) && isset($quoteMap[$itemData['warehouse_name']])) { $itemData['theoretical_amount'] = $quoteMap[$itemData['warehouse_name']]; $itemData['fee_difference'] = $itemData['billing_amount'] - $quoteMap[$itemData['warehouse_name']]; } else { $itemData['theoretical_amount'] = ''; $itemData['fee_difference'] = ''; //Log::warning('【逆向配送费核对仓库名称不匹配】--月份为:' . $yearMonth . "的费用核对匹配不到逆向配送费报价表的仓库名称,明细数据id为" . $itemData['id']); } $batchData[] = $itemData; unset($item, $itemData); // 释放单条数据内存 } // 使用模型批量更新 if (!empty($batchData)) { $itemizationModel->saveAll($batchData); } // 释放批次数据内存并触发垃圾回收 unset($items, $batchData); gc_collect_cycles(); }); } catch (\Exception $e) { print_r("报错了直接干死整个进程,具体报错信息如下:".$e->getMessage());die; //Log::error('【逆向配送费核对异常】--月份为' . $yearMonth . "的费用核对发生错误:" . $e->getMessage()); $status = false; } // 释放主函数中的大型变量 unset($quoteList, $quoteMap, $itemizationModel); gc_collect_cycles(); return $status; } ``` ### 其他建议: 1. **监控MySQL状态**:在运行过程中,监控MySQL的状态,特别是连接数和资源使用情况。 2. **查看MySQL错误日志**:错误日志(/var/log/mysql/error.log)中可能有更详细的错误信息。 3. **增加max_allowed_packet**:在MySQL配置文件中添加或修改: ``` max_allowed_packet = 256M ``` 然后重启MySQL。 4. **考虑使用事务**:当前代码没有使用事务,如果不需要事务,可以保持。但使用事务可能会增加连接的开销,所以这里不推荐。 5. **优化查询**:确保`ItemizationModel`的查询使用了索引,特别是`business_time`字段,以加快查询速度,从而减少连接占用时间。 ### 总结: 最可能的原因是`max_allowed_packet`设置过小,导致批量更新时数据包超过限制。其次是连接超时,但代码中已经设置了重连机制,所以调整重连间隔和增大`max_allowed_packet`应该能解决问题。 请先检查MySQL的`max_allowed_packet`设置,并适当增大。同时,调整重连间隔为更小的值(如50批)进行尝试。 如果问题仍然存在,请查看MySQL错误日志以获取更详细的信息。 --- §§相关问题§§: 1. 如何查看MySQL的`max_allowed_packet`当前设置? 2. 在ThinkPHP中,除了使用`chunk`方法,还有哪些处理大数据集的方法? 3. 如何监控MySQL连接状态和资源使用情况? 4. 在PHP中,如何实现数据库连接断开后的自动重连?</think>### 问题分析:SQLSTATE[HY000]: General error: 2006 MySQL server has gone away 这个错误通常由以下原因引起: 1. **连接超时**:MySQL服务器在空闲超时后关闭了连接 2. **数据包过大**:单次查询/更新数据超过`max_allowed_packet`限制 3. **服务器资源不足**:内存或线程资源耗尽 4. **长时间事务**:批量操作执行时间超过MySQL等待时间 ### 结合您的配置和代码,主要问题集中在: #### 1. 连接超时问题 - 虽然设置了`wait_timeout=28800`(8小时),但PHP脚本执行时间可能超过MySQL的`wait_timeout` - 代码中的重连机制每100批(1000条)才重连一次,处理80万条数据需要800次循环 - **计算**:若每批处理耗时0.5秒,总耗时≈400秒(6.7分钟),远小于8小时 - **结论**:连接超时不是主因 #### 2. 数据包过大问题(最可能原因) - 批量更新时`$batchData`数组可能过大 - 默认`max_allowed_packet=4MB`(未在配置中显式设置) - **代码问题**:`saveAll()`方法可能生成超大SQL语句 ```php // 问题代码片段 $batchData[] = $itemData; // 累计数据 $itemizationModel->saveAll($batchData); // 批量执行 ``` - 当`$batchData`包含大量记录时,生成的UPDATE语句可能超过MySQL包大小限制 #### 3. 资源限制问题 - PHP内存1024MB足够处理10条/批 - MySQL的`max_connections=10000`足够 - 但未设置`max_allowed_packet`和`innodb_buffer_pool_size` #### 4. 长时间事务问题 - 批量更新未分拆事务 - 整个操作作为单个长事务执行 ### 解决方案 #### 1. 优化数据库配置(mysql.conf) ```ini [mysqld] # 增加数据包大小限制 max_allowed_packet = 256M # 增加缓冲区大小 innodb_buffer_pool_size = 1G # 减少超时时间但增加重试能力 wait_timeout = 600 interactive_timeout = 600 net_write_timeout = 120 net_read_timeout = 120 ``` #### 2. 重构PHP代码(关键修改) ```php public function ReverseDeliveryFeeVerifyDo($yearMonth): bool { // ... [初始化部分不变] ... $batchCount = 0; $reconnectInterval = 50; // 增加重连频率 try { ItemizationModel::whereBetween('business_time', [ $monthTimeRange['startTime'], $monthTimeRange['endTime'] ])->field('id,billing_amount,warehouse_name,theoretical_amount,fee_difference') ->chunk(100, function ($items) use ($quoteMap, &$status, &$batchCount, $reconnectInterval) { // 增大每批数量 $batchCount++; if ($batchCount % $reconnectInterval === 0) { $this->reconnectDatabase(); } // 逐条处理替代批量数组 foreach ($items as $item) { $updateData = []; if (!empty($item->warehouse_name) && isset($quoteMap[$item->warehouse_name])) { $updateData['theoretical_amount'] = $quoteMap[$item->warehouse_name]; $updateData['fee_difference'] = $item->billing_amount - $quoteMap[$item->warehouse_name]; } else { $updateData['theoretical_amount'] = null; $updateData['fee_difference'] = null; } // 单条更新替代批量saveAll Db::table('itemization_table') ->where('id', $item->id) ->update($updateData); unset($updateData); // 及时释放内存 } unset($items); gc_collect_cycles(); }); } catch (\Exception $e) { // ... [错误处理] ... } // ... [清理资源] ... return $status; } ``` #### 代码优化说明: 1. **逐条更新替代批量更新** - 解决`max_allowed_packet`超限问题 - 避免生成巨型SQL语句 2. **增大chunk尺寸** - 从10条/批 → 100条/批 - 减少数据库查询次数(100万条数据从10万次查询 → 1万次) 3. **增加重连频率** - 每50批(5000条)重连一次 - 防止连接池回收 4. **使用直接SQL更新** - 避免ORM开销:`Db::table()->update()`替代`saveAll()` - 减少内存占用 #### 3. 补充优化建议 - **临时禁用查询日志**:`SET GLOBAL general_log = 'OFF'` - **增加索引**:确保`business_time`字段有索引 - **分时段处理**:将100万数据按周拆分执行 - **监控内存**:在循环中添加内存检查 ```php if (memory_get_usage() > 500 * 1024 * 1024) { gc_collect_cycles(); } ``` ### 配置验证步骤 1. **检查当前MySQL配置**: ```sql SHOW VARIABLES LIKE 'max_allowed_packet'; SHOW VARIABLES LIKE 'wait_timeout'; ``` 2. **压力测试脚本**: ```php // 测试数据库持久性 Db::query("SELECT SLEEP(600)"); // 测试10分钟连接 ``` 3. **监控工具**: ```bash # 实时监控MySQL连接 mysqladmin -uroot -p processlist ``` ### 最终建议方案执行顺序 1. 先修改MySQL配置并重启 2. 实施逐条更新代码改造 3. 增加chunk大小到100-500条 4. 添加分段处理逻辑(按ID范围分批) 5. 实施内存监控和强制GC 这些优化应该能解决"MySQL server has gone away"错误,同时提高大数据量处理的稳定性。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值