3

如何编码事务

 3 years ago
source link: https://blog.huoding.com/2018/04/24/663
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何编码事务

发表于2018-04-24

我说的事务指的是一般的数据库事务,而不是什么分布式事务之类高大上的概念。听起来很简单,但是即便如此,想实现的优雅一点也不是一件容易的事情。 

假设有一个 QA 系统,当用户在上面提问的时候,系统保存问题,然后更新用户的提问数,最后触发一个问题已经被创建的异步事件来解耦逻辑(代码均使用 Lumen 框架):

<?php

try {
    DB::beginTransaction();

    $question->content = '...';
    $question->save();

    $user->questions_count += 1;
    $user->save();

    DB::commit();

    event(new QuestionCreatedEvent($question));
} catch (Exception $e) {
    DB::rollBack();
}

?>

随着业务逻辑越来越复杂,会出现很多问题,其一:事务处理相关代码的割裂感会越来越严重;其二:事务处理相关逻辑会重复散落在很多地方,很容易遗漏或错乱。

如何解决问题?学院派面对此类问题,多半会搞出一个新的 service 层,专门用来处理事务,不过对我来说太重了,我需要的是更轻量级的方案,从 PSR-15 中可以找到答案,其中的 Middleware 机制构造出了一个类似洋葱皮的结构,通过它我们可以很容易的把事务处理的功能包裹在 controller 之上。

让我们看看如何实现事务处理的洋葱皮中间件:

<?php

namespace App\Http\Middlewares;

use Closure;
use Exception;

use Illuminate\Http\Request;
use Illuminate\Http\Response;

class TransactionMiddleware
{
    protected static $methods = [
        Request::METHOD_DELETE,
        Request::METHOD_PATCH,
        Request::METHOD_POST,
        Request::METHOD_PURGE,
        Request::METHOD_PUT,
    ];

    public function handle($request, Closure $next)
    {
        $method = $request->getMethod();

        if (! in_array($method, static::$methods)) {
            return $next($request);
        }

        $db = app('db');

        $db->beginTransaction();

        $result = $next($request);

        if ($result->getStatusCode() < Response::HTTP_BAD_REQUEST) {
            $db->commit();
        } else {
            $db->rollBack();
        }

        return $result;
    }
}

?>

说明:如上代码之所以没有使用 Lumen 中看是更简单的 DB::transaction() 方法,是因为在框架的工作流程中,异常在到达中间件之前就已经被处理消化掉了,所以在中间件里是捕获不到异常的,好在我们可以通过判断响应码来实现同样的效果。

激活事务处理的洋葱皮中间件之后,业务逻辑代码会得到极大简化:

<?php

$question->content = '...';
$question->save();

$user->questions_count += 1;
$user->save();

event(new QuestionCreatedEvent($question));

?>

如此一来,业务代码完全不用考虑事务处理了,中间件会通过 HTTP 方法来判断该请求是不是一个「写」请求,进而决定提交事务还是回滚事务。

不过洋葱皮中间件也带来了一个意想不到的问题:因为事务处理是包裹在外层的,所以 event 这个异步操作也被包裹到其中了,比如说:当我们创建了一个新问题,执行到异步的 event 的时候,事务本身还没有提交,于是在异步处理 event 的进程里,很可能取不到这个新创建的问题,从而导致失败。

为了解决这个问题,我们可以新建一个 register_event 方法来替换原本的 event 方法:

<?php

if (! function_exists('register_event')) {
    function register_event($event, $payload = [], $halt = false)
    {
        if (app()->runningInConsole()) {
            return event($event, $payload, $halt);
        }

        register_shutdown_function(function ()
            use ($event, $payload, $halt) {

            return event($event, $payload, $halt);
        });
    }
}

?>

如此一来,虽然异步事件相关的代码还是包裹在事务处理中的,但是它的执行时机却通过 register_shutdown_function 延迟到了最后,也就是说事务提交后才会执行,自然就不会出问题了。至于代码里为什么要判断是不是运行在命令行,其实是为了兼容 Lumen 测试框架中的 expectsEvents 方法,不是本文的重点,我就不多说了。

补充:关于 event 这个问题,我重新思考了一下,症结在于使用了 SerializesModels 机制,它会强制仅仅序列化 Model id,进而在反序列化的时候通过 id 来查询数据库得到数据。知道了这些,我们发现不使用 SerializesModels 机制即可规避问题。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK