引言
读写延迟”和“并发”问题,通常指的是在数据库操作中,特别是在主从复制架构或高并发场景下出现的经典问题。
下面将详细解析这些问题的表现、原因和解决方案:
问题表现
读写延迟(主从复制延迟)
-- 场景:刚写入的数据立即查询却查不到 -- 1. 写入主库 INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com'); -- 2. 立即从从库查询(可能查询失败) SELECT * FROM users WHERE email = 'zhangsan@example.com'; -- 查不到!
并发问题
// 场景:库存超卖 // 用户A和用户B同时购买最后一件商品 $stock = DB::table('products')->where('id', 1)->value('stock'); // 同时读到 stock=1 // 用户A if ($stock > 0) { DB::table('products')->where('id', 1)->decrement('stock'); // 处理订单... } // 用户B(同时执行) if ($stock > 0) { DB::table('products')->where('id', 1)->decrement('stock'); // 库存变成 -1! }
问题诊断流程

flowchart TD A[用户遇到数据不一致] --> B{检查问题类型} B -->|立即写入后查不到| C[读写延迟<br>主从同步问题] B -->|数据计数错误/超卖| D[并发竞争问题] B -->|随机性失败| E[网络/连接池问题] C --> F[解决方案: 强制主库读取] D --> G[解决方案: 事务锁/队列] E --> H[解决方案: 连接优化/重试]
下面详细说明各类问题。
1. 读写延迟(主从复制延迟)
原因分析
主从同步时间差:数据写入主库后,需要时间同步到从库网络延迟:主从服务器之间的网络延迟从库负载过高:从库处理查询请求过多,同步变慢大事务延迟:大量数据写入导致同步缓慢解决方案
方案1:强制从主库读取
// Laravel 中强制使用主库 // 方法1:使用 writeConnection $user = DB::connection('mysql::write')->table('users')->find(1); // 方法2:使用 onWriteConnection $user = User::onWriteConnection()->find(1); // 方法3:设置整个查询使用主库 DB::connection('mysql')->select('SET SESSION TRANSACTION READ WRITE'); $users = DB::table('users')->get();
方案2:使用 sticky 连接(Laravel 8.25+)
// config/database.php 'mysql' => [ 'sticky' => true, // 启用粘性连接 'read' => [ ['host' => 'read1.example.com'], ['host' => 'read2.example.com'] ], 'write' => [ 'host' => 'write.example.com' ], ], // 写入后的短时间内,同一请求的读操作会使用主库
方案3:延迟重试策略
function getWithRetry($id, $maxRetries = 3) { for ($i = 0; $i < $maxRetries; $i++) { $data = User::find($id); if ($data) { return $data; } // 延迟后重试 if ($i < $maxRetries - 1) { usleep(100000 * pow(2, $i)); // 指数退避 } } // 最后一次尝试从主库读取 return User::onWriteConnection()->find($id); }
2. 并发问题
原因分析
竞态条件:多个进程同时读取和修改同一数据无锁更新:UPDATE 操作没有适当的锁机制非原子操作:先读后写的非原子操作解决方案
方案1:使用数据库事务 + 行锁
// 悲观锁:SELECT ... FOR UPDATE DB::transaction(function () { // 锁定行 $product = DB::table('products') ->where('id', 1) ->lockForUpdate() // 行级锁 ->first(); if ($product->stock > 0) { DB::table('products') ->where('id', 1) ->decrement('stock'); // 创建订单... } }); // 或者使用 sharedLock(共享锁) $product = DB::table('products') ->where('id', 1) ->sharedLock() ->first();
方案2:使用原子操作
// 使用原子更新,避免先读后写 $affected = DB::table('products') ->where('id', 1) ->where('stock', '>', 0) ->decrement('stock'); if ($affected > 0) { // 库存减少成功,处理订单 } else { // 库存不足 } // 使用 CASE 语句 DB::table('products') ->where('id', 1) ->update([ 'stock' => DB::raw('CASE WHEN stock > 0 THEN stock - 1 ELSE stock END') ]);
方案3:使用 Redis 分布式锁
use Illuminate\Support\Facades\Redis; function purchaseWithLock($productId, $userId) { $lockKey = "purchase:lock:{$productId}"; $lock = Redis::set($lockKey, $userId, 'NX', 'EX', 10); // 10秒超时 if (!$lock) { throw new Exception('系统繁忙,请稍后重试'); } try { DB::transaction(function () use ($productId) { $product = Product::where('id', $productId) ->where('stock', '>', 0) ->first(); if ($product) { $product->decrement('stock'); // 创建订单... } }); } finally { Redis::del($lockKey); } }
方案4:使用队列串行处理
// 将并发请求转为串行处理 class ProcessPurchase implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function handle() { DB::transaction(function () { $product = Product::where('id', $this->productId) ->where('stock', '>', 0) ->first(); if ($product) { $product->decrement('stock'); // 创建订单... } }); } } // 触发购买 ProcessPurchase::dispatch($productId, $userId);
3. 混合问题解决方案
完整的最佳实践
class PurchaseService { public function purchase($productId, $userId, $quantity = 1) { // 1. 快速失败检查(不涉及数据库) if ($quantity <= 0) { throw new InvalidArgumentException('数量必须大于0'); } // 2. 使用数据库事务保证原子性 return DB::transaction(function () use ($productId, $userId, $quantity) { // 3. 使用行级锁防止并发 $product = Product::where('id', $productId) ->lockForUpdate() // 悲观锁 ->first(); if (!$product) { throw new ModelNotFoundException('商品不存在'); } if ($product->stock < $quantity) { throw new Exception('库存不足'); } // 4. 原子更新 $product->decrement('stock', $quantity); // 5. 创建订单 $order = Order::create([ 'user_id' => $userId, 'product_id' => $productId, 'quantity' => $quantity, 'status' => 'paid' ]); // 6. 清理缓存 Cache::forget("product:{$productId}"); return $order; }, 3); // 重试3次 } }
监控与诊断
监控指标
// 监控数据库延迟 class DatabaseMonitor { public static function checkReplicationDelay() { // 检查主从延迟 $delay = DB::select('SHOW SLAVE STATUS'); if ($delay['Seconds_Behind_Master'] > 5) { Log::warning('数据库主从延迟过高', [ 'delay' => $delay['Seconds_Behind_Master'], 'master' => config('database.connections.mysql.write.host'), 'slave' => config('database.connections.mysql.read.host') ]); } } public static function logSlowQueries() { // 启用慢查询日志 DB::enableQueryLog(); // 执行查询后 $queries = DB::getQueryLog(); foreach ($queries as $query) { if ($query['time'] > 1000) { // 超过1秒 Log::warning('慢查询', $query); } } } }
调试技巧
// 在代码中添加调试信息 DB::listen(function ($query) { Log::info('SQL Query', [ 'sql' => $query->sql, 'bindings' => $query->bindings, 'time' => $query->time, 'connection' => $query->connectionName ]); }); // 检查当前使用的连接 $connection = DB::getDefaultConnection(); $isWriting = DB::connection()->getPdo()->inTransaction();
总结与建议
问题快速定位
预防措施
设计阶段:
重要业务操作使用事务避免在事务中进行远程调用合理设计数据库索引开发阶段:
读写分离场景下,对一致性要求高的读操作强制走主库更新操作使用原子操作高并发场景使用队列或锁运维阶段:
监控主从延迟设置合理的超时时间定期优化数据库紧急处理
// 临时解决方案:全局强制走主库 // 在 AppServiceProvider 中 public function boot() { if (app()->environment('production') && request()->has('debug_read')) { DB::connection('mysql')->setReadConnection('write'); } }
如果您的具体问题是“写入后立即查询返回空”,那几乎可以确定是读写分离延迟问题,解决方案是让这个查询强制走主库连接。
以上就是MySQL读写延迟与并发导致的问题解决方案的详细内容,更多关于MySQL读写延迟与并发导致问题的资料请关注其它相关文章!
