page contents

Lumen 框架如何更优雅的使用事务

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

attachments-2020-06-K8nBjxic5ee9740d657be.png

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

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

 1<?php
 2
 3try {
 4    DB::beginTransaction();
 5
 6    $question->content = '...';
 7    $question->save();
 8
 9    $user->questions_count += 1;
10    $user->save();
11
12    DB::commit();
13
14    event(new QuestionCreatedEvent($question));
15} catch (Exception $e) {
16    DB::rollBack();
17}
18
19?>

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

如何解决问题?

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


v2-94d53b1ffbe052e5a235c4b648fda67c_720w.jpg


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

 1<?php
 2
 3namespace App\\Http\\Middlewares;
 4
 5use Closure;
 6use Exception;
 7
 8use Illuminate\\Http\\Request;
 9use Illuminate\\Http\\Response;
10
11class TransactionMiddleware
12{
13    protected static $methods = \[
14        Request::METHOD_DELETE,
15        Request::METHOD_PATCH,
16        Request::METHOD_POST,
17        Request::METHOD_PURGE,
18        Request::METHOD_PUT,
19    \];
20
21    public function handle($request, Closure $next)
22    {
23        $method = $request->getMethod();
24
25        if (! in_array($method, static::$methods)) {
26            return $next($request);
27        }
28
29        $db = app('db');
30
31        $db->beginTransaction();
32
33        $result = $next($request);
34
35        if ($result->getStatusCode() < Response::HTTP\_BAD\_REQUEST) {
36            $db->commit();
37        } else {
38            $db->rollBack();
39        }
40
41        return $result;
42    }
43}
44
45?>

说明:

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


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

 1<?php
 2
 3$question->content = '...';
 4$question->save();
 5
 6$user->questions_count += 1;
 7$user->save();
 8
 9event(new QuestionCreatedEvent($question));
10
11?>

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

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


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

 1<?php
 2
 3if (! function\_exists('register\_event')) {
 4    function register_event($event, $payload = \[\], $halt = false)
 5    {
 6        if (app()->runningInConsole()) {
 7            return event($event, $payload, $halt);
 8        }
 9
10        register\_shutdown\_function(function ()
11            use ($event, $payload, $halt) {
12
13            return event($event, $payload, $halt);
14        });
15    }
16}
17
18?>

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


补充:

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


attachments-2020-06-beDWYTcn5ee973e0ad28f.jpg

  • 发表于 2020-06-17 09:38
  • 阅读 ( 351 )
  • 分类:PHP开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1316 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章