Browse Source

修复作物灾害生成的并发问题

问题描述:
- 定时任务查询到可产生灾害的作物后,在处理过程中作物可能被用户铲除
- 导致定时任务仍然尝试为已删除的作物生成灾害,造成数据不一致

修复方案:
1. 优化generateDisasters方法执行流程:
   - 事务外进行复杂计算,减少锁时间
   - 事务内使用lockForUpdate进行有锁更新
   - 重新检查作物状态,如已删除则跳过

2. 移除不必要的土地状态检查:
   - 根据文档,作物灾害与土地无关
   - 简化业务逻辑,提高性能

3. 关键改进:
   - 使用DB::transaction()确保原子性操作
   - 使用lockForUpdate()防止并发修改
   - 重新查询作物状态确保数据一致性
   - 检查trashed()状态跳过已删除作物

修复效果:
- 解决并发时序问题
- 提高系统并发性能
- 保证数据一致性
- 避免为已删除作物生成灾害
dongasai 6 months ago
parent
commit
fe27c46edc
1 changed files with 49 additions and 35 deletions
  1. 49 35
      app/Module/Farm/Logics/DisasterLogic.php

+ 49 - 35
app/Module/Farm/Logics/DisasterLogic.php

@@ -13,6 +13,7 @@ use App\Module\Farm\Models\FarmLand;
 use App\Module\Farm\Models\FarmCropLog;
 use App\Module\Farm\Services\DisasterService;
 use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
 /**
@@ -114,10 +115,7 @@ class DisasterLogic
                 return null;
             }
 
-            // 跳过已经有灾害的土地
-            if ($land->status === LAND_STATUS::DISASTER) {
-                return null;
-            }
+            // 注意:根据文档,作物灾害和土地无关,不需要检查土地状态
 
             $userId = $crop->user_id;
             $seed   = $crop->seed;
@@ -277,6 +275,7 @@ class DisasterLogic
         $disasters = array_merge($disasters, $disasterInfos);
         $crop->disasters = $disasters;
 
+        // TODO: 根据文档,作物灾害和土地无关,此处更新土地状态可能需要重新评估
         // 更新土地状态为灾害
         $land = $crop->land;
         $oldLandStatus = $land->status;
@@ -371,12 +370,10 @@ class DisasterLogic
     public function generateDisasters(FarmCrop $crop): string
     {
         try {
-            // 更新检查时间
-            $crop->last_disaster_check_time = now();
+            // 1. 先进行基础检查和计算(不在事务中,减少锁时间)
 
-            // 检查用户是否存在,如果不存在则跳过
+            // 检查用户是否存在
             if (!$crop->user) {
-                $crop->save(); // 仍需更新检查时间
                 Log::warning('作物关联的用户不存在,跳过灾害生成', [
                     'crop_id' => $crop->id,
                     'user_id' => $crop->user_id
@@ -384,41 +381,58 @@ class DisasterLogic
                 return 'skipped';
             }
 
-            // 跳过已经有灾害的土地
-            if ($crop->land->status === LAND_STATUS::DISASTER) {
-                $crop->save(); // 仍需更新检查时间
-                return 'skipped';
-            }
+            // 注意:根据文档,作物灾害和土地无关,不需要检查土地状态
 
-            // 获取相关数据
+            // 获取相关数据并计算灾害
             $disasterResistance = $crop->seed->disaster_resistance ?? null;
-            $landDisasterResistance = ($crop->land->landType->disaster_resistance ?? 0) / 100; // 数据库存储百分比,需要除以100
+            $landDisasterResistance = ($crop->land->landType->disaster_resistance ?? 0) / 100;
             $activeBuffs = $crop->user->buffs->pluck('buff_type')->toArray();
 
             // 尝试生成灾害(支持多种灾害)
             $disasterInfos = $this->tryGenerateDisasterForCrop($crop, $disasterResistance, $landDisasterResistance, $activeBuffs);
 
-            if (!empty($disasterInfos)) {
-                // 应用灾害到作物
-                $this->applyDisastersToCrop($crop, $disasterInfos);
-
-                // 生成灾害后,设置当前阶段不能再产生灾害
-                $crop->can_disaster = false;
-                $crop->save();
-
-                Log::info('单个作物灾害生成成功', [
-                    'crop_id' => $crop->id,
-                    'user_id' => $crop->user_id,
-                    'disaster_count' => count($disasterInfos),
-                    'disaster_types' => array_column($disasterInfos, 'type')
-                ]);
+            // 2. 开启事务进行有锁更新
+            return DB::transaction(function () use ($crop, $disasterInfos) {
+                // 使用行锁重新获取作物,确保数据一致性
+                $lockedCrop = FarmCrop::where('id', $crop->id)
+                    ->lockForUpdate()
+                    ->first();
+
+                // 如果作物不存在或已被软删除,跳过处理
+                if (!$lockedCrop || $lockedCrop->trashed()) {
+                    Log::info('作物已被删除,跳过灾害生成', [
+                        'crop_id' => $crop->id,
+                        'user_id' => $crop->user_id
+                    ]);
+                    return 'skipped';
+                }
 
-                return 'generated';
-            } else {
-                // 没有生成灾害,但需要保存检查时间
-                $crop->save();
-                return 'checked';
-            }
+                // 更新检查时间
+                $lockedCrop->last_disaster_check_time = now();
+
+                // 如果有灾害需要生成,应用到作物
+                if (!empty($disasterInfos)) {
+                    // 应用灾害到作物
+                    $this->applyDisastersToCrop($lockedCrop, $disasterInfos);
+
+                    // 生成灾害后,设置当前阶段不能再产生灾害
+                    $lockedCrop->can_disaster = false;
+                    $lockedCrop->save();
+
+                    Log::info('单个作物灾害生成成功', [
+                        'crop_id' => $lockedCrop->id,
+                        'user_id' => $lockedCrop->user_id,
+                        'disaster_count' => count($disasterInfos),
+                        'disaster_types' => array_column($disasterInfos, 'type')
+                    ]);
+
+                    return 'generated';
+                } else {
+                    // 没有生成灾害,但需要保存检查时间
+                    $lockedCrop->save();
+                    return 'checked';
+                }
+            });
 
         } catch (\Exception $e) {
             Log::error('单个作物灾害生成失败', [