服务端模块化架构设计RPC模块化设计与分布式事务
模块间的调用问题
由于我们的模块是可以任意组合的,所以就会有一个问题:当两个模块是打包在一起的时候,相当于是内部调用当两个模块是在两个不同的服务的时候,就变成的远程调用
也就是说,我们需要为每一种组合都适配一遍
这不要了命了么?
不要急,我有办法,一套代码适配两种情况用户接口示例
我们在之前实现的juejin-pin(沸点)模块中,就有用户模型,比如发布沸点的用户,评论的用户等等
而用户的相关业务我们有单独的juejin-user(用户)模块,所以juejin-pin(沸点)模块中的用户信息就需要从juejin-user(用户)模块中获取
这里就会出现我们之前说的问题
如果juejin-pin(沸点)和juejin-user(用户)是合并在一起的,就像juejin-appliaction-single,那么可以直接进行内部调用
如果juejin-pin(沸点)和juejin-user(用户)是分开的,就像juejin-appliaction-system和juejin-appliaction-pin,那么需要通过远程服务调用抽象模块接口
对于获得用户信息这个功能,我们先定义一个接口UserApipublic interface UserApi { /** * 通过id获得用户信息 */ UserRO get(String id); } 复制代码
其中UserRO是user remote object,表示远程的,非本模块的用户对象
我们可以把这个接口放在juejin-basic中,这样其他的模块也能进行复用RemoteUserRepository
我们为juejin-pin(沸点)模块中的UserRepository实现一个RemoteUserRepository@Repository public class RemoteUserRepository implements UserRepository { @Autowired private UserApi userApi; /** * 根据 id 获得一个领域模型 */ @Override public User get(String id) { return ro2do(userApi.get(id)); } public User ro2do(UserRO ro) { //模型转换 } //省略其他代码 } 复制代码
当我们的juejin-pin(沸点)模块调用UserRepository#get(id)时,实际是通过UserApi#get(id)来获得用户信息,再通过ro2do将UserRO转为我们juejin-pin(沸点)模块中指定的User模型实现UserApi
接下来我们分别实现内部调用和远程服务调用这两种用户获取方式InnerUserApi
在juejin-user(用户)模块中实现InnerUserApi@Component public class InnerUserApi implements UserApi { /** * 这个是juejin-user中的UserRepository */ @Autowired private UserRepository userRepository; /** * 这个是juejin-user中的UserFacadeAdapter */ @Autowired private UserFacadeAdapter userFacadeAdapter; @Override public UserRO get(String id) { User user = userRepository.get(id); return userFacadeAdapter.do2ro(user); } } 复制代码
我们只需要直接调用UserRepository就行了
这条链路是这样的:
如果模块是合并的,那么直接通过内部的juejin-user(用户)模块提供的InnerUserApi就能获得用户信息了FeignUserApi
在juejin-basic(基础)模块中实现FeignUserApipublic class FeignUserApi implements UserApi { @Autowired private UserFeignClient userFeignClient; @Override public UserRO get(String id) { Response response = userFeignClient.get(id); if (response.isSuccess()) { return response.getObject(); } throw new RuntimeException(response.getMessage()); } } @FeignClient(name = "juejin-user") public interface UserFeignClient { @GetMapping("/user/{id}") Response get(@PathVariable String id); } 复制代码
这里需要集成UserFeignClient,通过Feign的方式来获得用户的信息
这条链路是这样的:
如果模块间是分开的,分别位于不同的服务中,就需要通过Feign等RPC方式了Feign路由映射
我们的juejin-user(用户)模块对应的服务实际上是juejin-appliaction-system,或者是其他的名称(不同的模块组合可能会有不同的命名)
但是如果每种组合方式都要手动修改对应的名称,那肯定不行,太麻烦了
我们可以看到在上面的示例中指定为对应的模块名称juejin-user,也就是UserFeignClient上的注解@FeignClient的参数是juejin-user
但是只是这样还不行,毕竟我们没有一个叫juejin-user的服务
所以我们要想办法让juejin-user能够根据不同模块组合动态的映射为对应的服务名称
这个功能其实我们已经在 网关路由模块化支持与条件配置 实现过了,大概的流程是在build.gradle中添加额外的脚本生成router.properties,其中记录当前服务包含的模块processResources { //资源文件处理之前 doFirst { Set mSet = new HashSet<>() //遍历所有的依赖 project.configurations.forEach(configuration -> { configuration.allDependencies.forEach(dependency -> { //如果是我们项目中的业务模块则添加该模块名称 if (dependency.group == "com.bytedance.juejin") { mSet.add(dependency.name) } }) }) //移除,基础模块不需要路由 mSet.remove("juejin-basic") //如果包含了业务模块 if (!mSet.isEmpty()) { //获得资源目录 File resourcesDir = new File(project.projectDir, "/src/main/resources") //创建路由文件 File file = new File(resourcesDir, "router.properties") if (!file.exists()) { file.createNewFile() } //将模块信息写入文件 Properties properties = new Properties() properties.setProperty("routers", String.join(",", mSet)) OutputStream os = new FileOutputStream(file) properties.store(os, "Routers generated file") os.close() } } } 复制代码读取router.properties将数据同步到注册中心@Component public class RouterRegister { /** * 监听服务注册前置事件 */ @EventListener public void register(InstancePreRegisteredEvent event) throws Exception { //读取 router.properties 资源文件 ClassPathResource resource = new ClassPathResource("router.properties"); //加载到 Properties 中 Properties properties = new Properties(); try (InputStream is = resource.getInputStream()) { properties.load(is); } //获得 routers 值 String routers = properties.getProperty("routers"); //写入 metadata 中 Map metadata = event.getRegistration().getMetadata(); metadata.put("routers", routers); } } 复制代码
(上面两块更详细的内容可以看 网关路由模块化支持与条件配置 中的实现)监听心跳事件刷新模块和服务的映射关系
这里我们只要把网关的路由刷新逻辑移过来就行了@Slf4j public class RouterLoadBalancerClientFactory extends LoadBalancerClientFactory { private final DiscoveryClient discoveryClient; private volatile Map routerMap = Collections.emptyMap(); public RouterLoadBalancerClientFactory(LoadBalancerClientsProperties properties, DiscoveryClient discoveryClient) { super(properties); this.discoveryClient = discoveryClient; } @Override public T getInstance(String name, Class type) { String router = getRouter(name); log.info("Router mapping: {} => {}", name, router); return super.getInstance(router, type); } protected String getRouter(String name) { return routerMap.getOrDefault(name, name); } /** * 监听心跳事件 */ @EventListener public void refreshRouters(HeartbeatEvent event) { //新的路由映射 Map newRouterMap = new HashMap<>(); //获得服务名 List services = discoveryClient.getServices(); for (String service : services) { //获得服务实例 List instances = discoveryClient.getInstances(service); if (instances.isEmpty()) { continue; } //这里直接拿第一个 ServiceInstance instance = instances.get(0); //获得 metadata 中的 routers String routersMetadata = instance.getMetadata() .getOrDefault("routers", ""); String[] routers = routersMetadata.split(","); for (String router : routers) { newRouterMap.put(router, service); } } if (!this.routerMap.equals(newRouterMap)) { log.info("Update router map => {}", newRouterMap); } //更新缓存 this.routerMap = newRouterMap; } } 复制代码
通过监听服务注册的心跳,同步模块和服务的映射关系
扩展LoadBalancerClientFactory,在中间添加一步将模块名称映射为服务名称的逻辑
这里高版本的Spring Cloud用的是spring-cloud-loadbalancer做的负载均衡,所以我们扩展LoadBalancerClientFactory就行了
如果是低版本,用的是ribbon,扩展的类是不一样的,有需要的话可以看 【Spring Cloud】协同开发利器之动态路由|Ribbon & LoadBalancer 解析篇,也可以参考这个库的源码来扩展ribbon条件配置
最后还需要添加一个配置类@Configuration @AutoConfigureBefore(LoadBalancerAutoConfiguration.class) @EnableFeignClients(basePackages = "com.bytedance.juejin.basic.rpc.feign") public class FeignAutoConfiguration { @Bean @ConditionalOnMissingBean public UserApi userApi() { return new FeignUserApi(); } @Bean public LoadBalancerClientFactory routerLoadBalancerClientFactory(LoadBalancerClientsProperties properties, DiscoveryClient discoveryClient) { return new RouterLoadBalancerClientFactory(properties, discoveryClient); } } 复制代码
用@ConditionalOnMissingBean标记FeignUserApi
当juejin-pin(沸点)和juejin-user(用户)是合并在一起的时候,Spring会识别到InnerUserApi,于是不会注入FeignUserApi,所有的用户接口都会走本地用户模块的UserRepository
当juejin-pin(沸点)和juejin-user(用户)是分开的时候, FeignUserApi会被注入,所有的用户接口都会走Feign
这样我们只需要根据需求定义对应的xxApi,然后分别实现InnerApi和FeignApi或是DubboApi的方式,之后无论我们对模块进行怎么样的自由组合都能够自动适配,不需要额外的手动处理分布式事务问题
如果我们的模块间调用需要用到分布式事务是否存在一些方式能够做到兼容呢,当两个模块合并在一起的时候就用本地事务,当两个模块分开的时候就用分布式事务,根据模块间的组合方式自动识别切换
目前我的答案是不太好做(当然如果有大佬想到比较好的方式也可以分享一下)
现在有如下的代码@PostMapping("/test") @SmartTransactional//我们自己实现事务切面 public void test() { a.a();//本地调用 b.b();//本地调用或服务间调用 } 复制代码
如果我们自己实现事务切面
我们什么时候能知道是不是服务间调用?b.b()调用的时候,我们可以根据不同的实现确定是本地调用还是服务间调用
当我们调用b.b()确定了服务间调用需要选择分布式事务的时候,a.a()已经执行了
所以我们其实没办法在方法开始之前确定方法中是否会有服务间调用,更何况还会有嵌套事务等复杂场景
如果一定要用分布式事务的话,还是单独处理比较好,可以额外加一个方法@PostMapping("/test-local") @Transactional public void testLocal() { a.a();//本地调用 b.b();//本地调用 } @PostMapping("/test-seata") @GlobalTransactional public void testSeata() { a.a();//本地调用 b.b();//服务间调用 } 复制代码
这样的写的话也不需要频繁修改,只需要让前端调不同的接口就行了
而且一般来说需要用到分布式事务的也就几个核心场景,不会特别多
所以这种方式虽说加入了一些人工判断但应该也不会特别麻烦总结
要一套代码适配不同的场景其实就是定义一个接口然后进行多种实现,其优势在于借助接口的特性在不同场景下适配不同的实现,不仅不需要频繁修改代码,还可以实现InnerUserApi,FeignUserApi,DubboUserApi等多种方式,甚至其他系统的用户信息,如DouYinUserApi
同时借助已有的组件为我们服务,如Spring的条件配置,注册中心的组件能力等
8万左右的车,有什么推荐吗?经济汽车选择顺序一定是轿车优于SUV,同等价位轿车的配置一定是比SUV高的,而且功能也更齐全。如果一定要买SUV那就选自己喜欢的,因为同等价位的SUV都差不多。比如一个品牌在你看到
一提到湖南省永州市,你首先会想到什么?我曾经是一个吃货,所以现在十分悲哀地得了糖尿病!捂脸一提到湖南省永州市,我首先会到永州血鸭和东安鸡。永州血鸭是湖南永州当地家家户户都会做的一道传统名菜。仔鸭下锅翻炒,淋入新鲜的鸭血
哈尔滨最值得吃的饭店都有哪些?酒香不怕巷子深!作为一个哈尔滨的本地人,必须要为你推荐一些本地人经常去吃的饭店,好吃到不行!推荐一轩辕回民。哈尔滨的回民饭店还是相当有特色的,这家是一家隐藏在小区里的小饭店。门脸不
准备西安到海南自驾游,请问各位大侠能否介绍沿途景区?我推荐的路线如下西安十堰襄阳常德永州桂林湛江海口一为什么推荐这条线路呢?1这是一条旅游文化的线路读万卷书,行万里路这样的古语可以用在我本人身上,我是非常喜欢这条线路的。我出门旅游的
青岛有哪些好吃的餐厅推荐?赵家牛肉砂锅据说这是全青岛最好吃的牛肉砂锅,是老字号听说老板曾经在团岛早市卖了十几年的甜沫,但是却做得一手超级好吃的牛杂砂锅。用原汤炖的牛杂超级入味,超级香。牛肉每一块都带着筋,特
家庭宽带短期内还有可能上升到1000兆以上吗?目前我这里农宽带最低100兆,普遍300兆。公司正在大规模推广1000兆宽带。别人那些理论和想象我不知道怎么来的结果,我这个是正在发生和已经发生的事实!可以上海有5000兆的你不在
是不是只要注册了滴滴,然后跑过几单滴滴,保险公司就不理赔了?具体还要看签的合同,我翻了一下我的保险合同的确有这么一段话该车出险时,如为营业性用途,我公司不承担一切赔偿责任。法律的条款写得很清楚,但是解释的权利归谁却很模糊。那么保险公司是如何
我考上了山师的提前批,但我如果不报提前批我可以去东北电力大学,我有点后悔报提前批怎么办?我家闺女也是提前批录到山师定向济南,报之前我们充分了解过公费生的利与弊,毕竟是关乎孩子人生的重大决择我和孩她爸慎之又慎。因为孩子的分数挺尬尴不上不下,走211也只能录些工科类专业甚
高一入学考试重要吗?认真准备下。高一入学考试,主要是为摸底,根据学生实际层次再微调。因为中考科目多,加之水分大,学生的实际水平并不见得很明朗。一些好学校,可能会自己就语数外物等主要学科进行再测,本校老
焦虑症每天应该干什么比较好?患有焦虑症的人到底要怎样做?不看不知道。有一位网友在网络发出了自己患有焦虑症并且痊愈的经历,文字如下最初诊断为焦虑症,然后,服用一种叫做文拉法辛的药物,直到去年才停止服药。切记,药
骨折后会有什么后遗症吗?回答这个问题之前,想给大家提个问题您认为我们的身体之所以能灵活活动最关键的结构是什么?相信看了上面的这个动图,大家应该就会明白答案了,那就是我们的关节。无论是我们的颈椎,还是上肢下