记一次hyperf 微服务实践

简单的业务模型图

记一次hyperf 微服务实践_第1张图片

这里对分层进行简单的说明:

  • 接口层

    • 提供统一的http请求入口
    • 验证用户身份和请求的参数,对用户请求进行过滤。
    • 通过rpc调用业务层的方法来组织业务逻辑
    • 本身不对数据层进行直接操作
    • 从consul/etcd中发现服务提供方
  • 业务层

    • 实现业务逻辑,并且为接口层提供rpc调用服务
    • 定时任务,消息队列的生产和消费
    • 使用数据层将业务的结果持久化保存到数据库中
    • 将服务注册到consul/etcd中
  • 数据层

    • mysql 数据库提供主要的数据存储能力和事务处理能力
    • mongo 数据库提供数据归档能力
    • amqp 提供消息队列支持
    • elasticsearch 提供搜索服务和日志存储
  • 公共服务

    • 接口层和业务层都可能会用到redis提供缓存
    • 接口层和业务层都需要进行日志的收集和持久化
  • 注册发现

    • 这里由于hyperf框架的支持,选择使用consul作为服务的注册和发现
    • 开发阶段使用注册发现有很多不便,这里就通过svc的节点的方式进行rpc调用

示例源码

从官方demo开始说起

下面是官方实例的一个服务调用

__request(__FUNCTION__, compact('a', 'b'));
    }
}

我们看他的__request方法:

protected function __request(string $method, array $params, ?string $id = null)
{
    if (! $id && $this->idGenerator instanceof IdGeneratorInterface) {
        $id = $this->idGenerator->generate();
    }
    $response = $this->client->send($this->__generateData($method, $params, $id));
    if (is_array($response)) {
        $response = $this->checkRequestIdAndTryAgain($response, $id);

        if (array_key_exists('result', $response)) {
            return $response['result'];
        }
        if (array_key_exists('error', $response)) {
            return $response['error'];
        }
    }
    throw new RequestException('Invalid response.');
}

如果按照这个例子去组织代码你就会发现一个问题,如果result中也包含error里面的字段该怎么办?

比如:

  • 某个业务成功的返回值中包含 codemessage, data 字段,那么根据返回值你怎么判断成功还是失败,总不能要求业务中不能返回这三个字段吧。

你可能会想把resutl和error定义成相同的数据结构,然后根据业务中的code的取值范围定义不同的错误

于是我扛起锄头写下下面的代码:

code = $response["code"];
        $this->message = $response["message"];
        $this->data = $response["data"];
    }
    //判断是否请求成功
    public function isOk(): bool
    {
        return $this->code == 0 ;
    }
    //获取响应的消息
    public function getMessage(): string
    {
        return $this->message;
    }
    //获取响应的数据
    public function getData()
    {
        return $this->result;
    }
}

将响应的结果封装成一个ServiceResponse对象,然后提供几个方法。

你觉得这样可以吗?能用但是不规范!而且对于结果还有有各种if判断,感觉太糟糕了

使用rpc的意义就在于像调用本地方法一样调用远程系统的方法, 现在这个样子简直就是背道而驰!

于是经过一番研究突然发现了一个ServiceClient继承了AbstractServiceClient

没错,这个才是响应的正确处理方法,原来作者造就考虑到了!所以当你要请求一个服务的时候应该这样子:

那么,接下来我们就动手做一个手机号登录的服务

做一个手机号登录的业务

接口的定义

首先我们先定义一套远程方法的接口,同时下发给服务的提供者和服务的消费者

API统一响应

 0 ,
            "msg" => $message,
            "data" => $data,
        ];
    }

    /**
     * 出错的响应
     * @param int $code
     * @param string $message
     * @param array $data
     * @return array
     */
    public function error(int $code , string $message = '', array $data = [] )
    {
        return [
            "code" => $code ,
            "msg" => $message,
            "data" => $data,
        ];
    }

    /**
     * 验证失败
     * @param Validator $validator
     * @return array
     */
    public function fails(Validator $validator)
    {
        return [
            "code" => 100,
            "msg" => "validate error !",
            "data" => [
                "errors" => $validator->errors()
            ],
        ];
    }
}

API调用rpc服务

container->get(ValidatorFactory::class)->make($request->all(),[
            "phone" => "required",
            "code" => "required",
        ]);

        if($validator->fails()){
            return $this->fails($validator);
        }
        $params = $validator->validated();

        $result = make(UserBaseServiceInterface::class)->loginWithPhoneCode($params["phone"],$params["code"]);

        //登录失败会抛出异常,能走到这里就是成功的,接下来给用户生成jwt-token
        $jwt = $this->container->get(JwtFactory::class)->make();

        return $this->success([
            "token" => $jwt->fromSubject(new JwtSubject($result["user_id"],$result)),
        ]);
    }

    /**
     * 手机号码检测
     * @param RequestInterface $request
     * @return array
     */
    public function check(RequestInterface $request)
    {
        $validator = $this->container->get(ValidatorFactory::class)->make($request->all(),[
            "phone" => "required",
        ]);

        if($validator->fails()){
            return $this->fails($validator);
        }
        $params = $validator->validated();

        $result = make(UserBaseServiceInterface::class)->phoneLoginCheck($params["phone"]);

        return $this->success([
            "check" => $result,
        ]);
    }

    /**
     * 发送手机验证码
     * @param RequestInterface $request
     * @return array
     */
    public function code(RequestInterface $request)
    {
        $validator = $this->container->get(ValidatorFactory::class)->make($request->all(),[
            "phone" => "required",
        ]);

        if($validator->fails()){
            return $this->fails($validator);
        }
        $params = $validator->validated();

        $result = make(UserBaseServiceInterface::class)->sendLoginPhoneCode($params["phone"]);

        return $this->success([
            "send_at" => $result ? time() : 0 ,
        ]);
    }
}

服务提供者

这里都省略了数据库操作和业务逻辑部分,直接返回了结果

container = $container;
    }

    public function phoneLoginCheck(string $phone): int
    {
        //没有注册
        return 0;
    }


    public function sendLoginPhoneCode(string $phone): bool
    {
        $code = "123456";

        if(env("APP_ENV") == "prod" ){
            //在服务中调用服务
            $this->container->get(Service\SmsCodeServiceInterface::class)->sendLoginVerifyCode($phone,$code);
        }
        return true;
    }


    public function loginWithPhoneCode(string $phone, string $code): array
    {
        //TODO 检查短信验证码,根据手机号查找用户

        return [
            "user_id" => rand(111111111,9999999999),
            "user_name" => sprintf("user-%s",uniqid()),
        ];
    }

    public function getUserInfoById(int $id): array
    {
        //TODO  根据id查找用户
        return [
            "user_id" => $id,
            "user_name" => sprintf("user-%s",uniqid()),
            "level" => 99,
        ];
    }
}

小结

至此我们就组织起了一个完整的逻辑:通过api层调用服务svc层然后将结果响应给接口。

但是,业务并不总是成功的,api层虽然对参数进行了校验,却有可能会出现业务逻辑的异常。

例如:

  • 使用手机验证码进行登录,但是手机号被系统拉黑了,不能正常登录
  • 当提交一笔订单的时候,商品库存不足了,无法下单

这么这些业务逻辑的错误怎么告诉调用者?使用数组返回吗?不!如果这样做了消费者又要陷入到各种逻辑的判断中了,所以这里我们使用抛出异常的方式进行返回。只要能让异常也像调用本地方法一排抛出,我们就可以在api层中针对性的捕获各种业务上的异常,然后将异常转换为api的响应格式,如此这般我们就可以在消费者中只处理成功的逻辑。

接下来我们就加入异常的处理逻辑

定义一个异常类

在api和svc中定义一个App\Exception\LogicException类,专门处理业务中出现的各种异常。
api使用rpc调用的服务中抛出这个异常时,api中也会抛出这个异常。这样就实现了异常的传递

定义一套错误码

定义错误码是很有必要的,尤其是多语言的项目,可以根据错误码返回对应语言的文字提示

在服务中抛出LogicException异常

 rand(111111111,9999999999),
            "user_name" => sprintf("user-%s",uniqid()),
        ];
    }
    
    .......
}

API层将LogicException异常转换为响应

stopPropagation();
        $code = $throwable->getCode();
        $message = $throwable->getMessage();
        $data = [];

        $content = json_encode($this->error($code,$message,$data),JSON_UNESCAPED_UNICODE);

        return $response
            ->withAddedHeader('content-type', 'application/json; charset=utf-8')
            ->withBody(new SwooleStream($content));
    }

    public function isValid(Throwable $throwable): bool
    {
        return $throwable instanceof LogicException;
    }
}

关键代码

rpc服务中的异常处理

ResponseBuilder:创建出错的响应

formatErrorResponse($request, $code, $error));
        return $this->response()->withHeader('content-type', 'application/json')->withBody($body);
    }

    protected function formatErrorResponse(ServerRequestInterface $request, int $code, \Throwable $error = null): string
    {
        [$code, $message] = $this->error($code, $error ? $error->getMessage() : null);
        $response = $this->dataFormatter->formatErrorResponse([$request->getAttribute('request_id'), $code, $message, $error]);
        return $this->packer->pack($response);
    }
}

dataFormatter: 错误数据的格式化,NormalizerInterface是关键

normalizer = $normalizer;

        parent::__construct($context);
    }

    public function formatRequest($data)
    {
        $data[1] = $this->normalizer->normalize($data[1]);
        return parent::formatRequest($data);
    }

    public function formatResponse($data)
    {
        $data[1] = $this->normalizer->normalize($data[1]);
        return parent::formatResponse($data);
    }

    public function formatErrorResponse($data)
    {
        if (isset($data[3]) && $data[3] instanceof \Throwable) {
            $data[3] = [
                'class' => get_class($data[3]),
                'attributes' => $this->normalizer->normalize($data[3]),
            ];
        }
        return parent::formatErrorResponse($data);
    }
}

这里我们可以看出一个抛出异常的响应被转换成这个样子:

{
    "jsonrpc": "2.0",
    "id": "6234809b91ff9",
    "error": {
        "code": -32000,
        "message": "错误的手机验证码!",
        "data": {
            "class": "App\\Exception\\LogicException",
            "attributes": {
                "message": "错误的手机验证码!",
                "code": 10001,
                "file": "/opt/www/app/JsonRpc/UserBaseService.php",
                "line": 49
            }
        }
    },
    "context": []
}

API中RPC响应结果处理

这里将异常信息进行还原并抛出,关键还是NormalizerInterface

idGenerator instanceof IdGeneratorInterface && ! $id) {
            $id = $this->idGenerator->generate();
        }
        $response = $this->client->send($this->__generateData($method, $params, $id));
        if (! is_array($response)) {
            throw new RequestException('Invalid response.');
        }
        $response = $this->checkRequestIdAndTryAgain($response, $id);
        if (array_key_exists('result', $response)) {
            $type = $this->methodDefinitionCollector->getReturnType($this->serviceInterface, $method);
            if ($type->allowsNull() && $response['result'] === null) {
                return null;
            }

            return $this->normalizer->denormalize($response['result'], $type->getName());
        }
        if ($code = $response['error']['code'] ?? null) {
            $error = $response['error'];
            // Denormalize exception.
            $class = Arr::get($error, 'data.class');
            $attributes = Arr::get($error, 'data.attributes', []);
            if (isset($class) && class_exists($class) && $e = $this->normalizer->denormalize($attributes, $class)) {
                if ($e instanceof \Throwable) {
                    throw $e;
                }
            }
            // Throw RequestException when denormalize exception failed.
            throw new RequestException($error['message'] ?? '', $code, $error['data'] ?? []);
        }

        throw new RequestException('Invalid response.');
    }
}

关键配置

官方文档提到要 在 dependencies.php配置NormalizerInterface

 new SerializerFactory(Serializer::class),//(必须这样写)
];

并且 还要在composer.json中导入 symfony/serializer (^5.0)symfony/property-access (^5.0)

如果不配置的话LogicException异常是不能传递的,起初我以为只有rpc调用结果需要返回对象时才需要,所以没有配置,结果导致无法抛出LogicException,后来看了源码发现异常也是当做对象进行还原的。

你可能感兴趣的:(hyperf微服务php)