oceans ,英文 ocean 大海的复数。没有什么特殊含义,就觉得好听。
自己称这个项目为“与海”,渴望自由。
初衷是希望做一个 B2C 的商城 demo 项目,并且搭配一些可口的文章。但是,貌似困难有点大,所以很可能是不了了之。一个人堆代码,可能有点无趣。我的梦想是,星辰大海。
当然,无论怎么样,还是会通过这个 README 文件,简单介绍下这个项目。
考虑到大多数团队的体量,暂时没有必要引入 Gateway ,所以仅仅分成 bff 和 service 两层。其中:
- bff 层,负责原有网关功能,以及 Dubbo RPC 的 HTTP 封装、拼装等功能。
- service 层,实现具体的业务逻辑,提供 Dubbo RPC 接口。
TODO 此处等待老梁来补充。我去鞭笞老梁。哈哈哈哈
可参见如下几个类:
cn.iocoder.oceans.core.exception.ServiceException
cn.iocoder.oceans.core.util.ServiceExceptionUtil
cn.iocoder.oceans.user.service.config.ServiceExceptionConfiguration
cn.iocoder.oceans.user.api.constants.ErrorCodeEnum
cn.iocoder.oceans.core.constants.ErrorCodeEnum
原本在 User Passport 准备基于 Spring Security + Spring Security OAuth2 框架进行实现。但是考虑到,User Passport 使用 OAuth2 略显笨重,使用 Spring Security 也略显笨重。并且,User Passport 往往有比较飘逸的注册需求,例如:手机验证码登陆,并且未注册的情况下,自动注册。
所以考虑再三,并且尝试使用 Spring Security + Spring Security OAuth2 进行了一次实现后决定,还是选择自己基于 OAuth2 的**,实现一个 Lite 版。具体可参见如下几个类:
cn.iocoder.oceans.user.service.impl.OAuth2ServiceImpl
cn.iocoder.oceans.user.service.impl.MobileCodeServiceImpl
cn.iocoder.oceans.user.service.impl.UserServiceImpl
cn.iocoder.oceans.webapp.bff.interceptor.SecurityInterceptor
cn.iocoder.oceans.webapp.bff.context.SecurityContext
cn.iocoder.oceans.webapp.bff.context.SecurityContextHolder
cn.iocoder.oceans.webapp.bff.controller.passport.PassportController
cn.iocoder.oceans.webapp.bff.annotation.@PermitAll
当然,如上实现是直接基于 MySQL 实现,实际最好引入 Redis 进行缓存,以提高性能。
TODO 未完成
统一响应格式如下:
{
code: , // 错误码,使用全局错误码。成功时,使用 0 。
msg: , // 错误提示。成功时,返回 "" 。
data: // 具体业务数据。不定具体的格式,可以是 String ,也可以是对象,也可以是数组。
}
可参见类:
cn.iocoder.oceans.webapp.bff.vo.RestResult
cn.iocoder.oceans.webapp.bff.config.GlobalResponseBodyAdvice
cn.iocoder.oceans.webapp.bff.config.GlobalExceptionHandler
Java 各种 O ,也是心烦。
- Controller 返回,使用 VO 或者 DTO 。
- Dubbo Service API 入参和返回,统一 DTO 。
- Dubbo Service Impl 之间,可以返回 PO 。
- DAO 入参和返回,统一 PO 。
说起分布式,理论的文章很多,落地的实践很少。笔者翻阅了各种分布式事务组件的选型,大体如下:
- TCC 模型:TCC-Transaction、Hmily( How much i love you )
- XA 模型:Sharding Sphere、MyCAT
- 2PC 模型:raincat、lcn
- MQ 模型:RocketMQ
- BED 模型:Sharding Sphere
- Saga 模型:ServiceComb Saga
那怎么选择呢?目前社区对于分布式事务的选择,暂时没有定论,至少笔者没有看到。笔者的想法如下:
- 从覆盖场景来说,TCC 无疑是最优秀的,但是大家觉得相对复杂。实际上,复杂场景下,使用 TCC 来实现,反倒会容易很多。另外,TCC 模型,一直没有大厂开源,也是一大痛点。
- 从使用建议来说,MQ 可能是相对合适的( 不说 XA 的原因还是性能问题 ),并且基本轮询了一圈朋友,发现大多都是使用 MQ 实现最终一致性居多。
- 2PC 模型的实现,笔者觉得非常新奇,奈何隔离性是一个硬伤。
- Saga 模型,可以认为是 TCC 模型的简化版,所以在理解和编写的难度上,简单非常多。因为 ServiceComb Saga 好像不支持 Dubbo ,所以并未深入去研究。
最终,笔者选择了 MQ 模型。但是 MQ 模型会有一个问题,在于前置的资源锁定问题。那么怎么解决呢?笔者以下单业务为例,可参见代码如下:
cn.iocoder.oceans.order.service.impl.OrderServiceImpl
cn.iocoder.oceans.order.service.po.OrderPO
cn.iocoder.oceans.core.base.mq.TransactionMQProducer
大体流程是:
- 扣除库存(考虑模型简单,直接使用商品服务管理库存)
- 使用优惠劵
- 创建订单
实现方式:
- 在
OrderServiceImpl#createInitOrder(...)
方法中,事务 A ,创建一个处于初始化状态的订单。该状态下的订单,用户不可见。什么用呢?下面说。 - 在
OrderServiceImpl#initOrder(...)
方法中,事务 B,远程调用=>扣除库存,远程调用=>使用优惠劵,更新订单状态为待支付。那么可能会有胖友会说,远程调用会失败呢?确实是,并且更麻烦的是,这是跨进程跨事务,无法直接事务回滚!这个时候,初始化状态的订单的作用就出来了。后台定时任务OrderServiceImpl#pollingTimeoutOrderApply()
,不断轮询超过 n 分钟( 例如说 5 分钟 )未初始化完成的订单( 即订单状态修改为待支付的订单 )。理论来说,订单 n 分钟内肯定可以初始化完成,如果未初始化完成,说明已经失败了,所以需要回滚扣除的库存和使用的优惠劵。但是,要注意,回滚扣除库存的方法和使用的优惠劵的方法,需要保证幂等性,因为可能会存在重复调用。通过这样的方式,我们解决了,MQ 方式无法前置的资源锁定的问题。 - 考虑到订单创建成功后,可能还有后置的逻辑,例如说,通知商家、赠送积分、赠送优惠劵等等。并且考虑到性能,那么我们适合发送 MQ 消息。但是为了 MQ 消息的稳定送达,需要使用 RocketMQ 事务消息。详细见,
OrderServiceImpl#createOrder(...)
方法。 - 考虑到【2】中,使用定时任务轮询超时初始化的订单,锁定的资源太久,所以在
OrderServiceImpl#createOrder(...)
方法中,在捕捉到OrderServiceImpl#initOrder(...)
方法中发生异常时,发送一条消息,异步取消资源的锁定。
😈 这块写的比较匆忙,如果有不理解的胖友,欢迎关注我的公众号【芋道源码】给我留言。因为时间关系,示例代码并没有完整实现完。嘿嘿。
😈 实际上,如果下单流程能够引入 TCC 模型的框架,代码会容易实现非常多。感兴趣的胖友,可以自己思考下。
TODO
TODO
TODO Apollo
TODO SkyWalking
TODO Sentinel
TODO Sharding Sphere
TODO Spring Cloud Gateway