V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
love2075904
V2EX  ›  Java

单体应用改造微服务疑惑求解

  •  
  •   love2075904 · 2022-02-16 15:01:44 +08:00 · 2658 次点击
    这是一个创建于 771 天前的主题,其中的信息可能已经有所发展或是发生改变。

    长期以来公司架构是基于 Spring 的单体应用,经过这么多年的发展,在企业应用开发层面沉淀颇多,对于新需求也能快速响应。但是由于目前公司业务多元化,单体应用的瓶颈就上来了,比如某个业务需要独立迭代,SAAS 改造的提出等,所以计划将单体升级改造为微服务架构。

    目前经过调研,整体升级代价不大,因为虽然是单体应用,但是依然使用多模块开发,各应用天然解耦。但是目前遇到几个比较麻烦的问题,看看有做过相同事情的 hxd 有没有解决方案。

    • 基于 Spring 容器的扩展机制

    为了降低模块之间的耦合度,系统中大量利用了 Spring 的一些特性,比如 getBeansOfType(),这种模式下,只需要在 A 模块定义好接口,在 B 和 C 模块定义好实现,即可在不同的业务场景下调用不同的实现。这种方式在同一个 Spring 上下中可以轻易获取实现类,但是微服务后就是多个上下文环境,就无法获取到这些实现类了。 伪代码如下:

    模块 A:

    
    public interface MessageSender {
        /**
         * 发送者唯一标识
         */
        String key();
        /**
         * 发送消息
         */
        void send(List<Message> messages);
    	
    }
    
    @Controller
    public class MessageController {
    
        private ApplicationContext ac;
    
        public void sendMessage(String key){
            List<MessageSender> messageSenders = ac.getBeansOfType(MessageSender.class);
            for(MessageSender sender: messageSenders){
                // 获取对应的实例
                if(Objects.equals(sender.getKey(), key)){
                	// 推送消息
                    sender.send(...);
                    break;
                 }
            }
        }
    
    }
    
    

    模块 B

    @Component
    public class EmailMessageSender implements MessageSender {
        /**
         * 发送者唯一标识
         */
        String key(){
        	return "email":
        }
        /**
         * 发送消息
         */
        void send(List<Message> messages){
           // 发送邮件
        }
    	
    }
    
    

    模块 C

    @Component
    public class SMSMessageSender implements MessageSender {
        /**
         * 发送者唯一标识
         */
        String key(){
        	return "sms":
        }
        /**
         * 发送消息
         */
        void send(List<Message> messages){
           // 发送短信
        }
    	
    }
    
    
    • 事件机制

    利用 Spring 的ApplicationEventPublisher实现事件推送,目前了解到可以通过对接Spring Cloud Bus实现跨服务事件推送。现在在我们应用中有同步事件和异步事件,异步事件使用@Async实现,目前没有了解到Spring Cloud Bus是否支持同步事件?

    模块 A

    public class MessageService{
      
        ApplicationEventPublisher publisher;
    
        public void sendMessage(){
            publisher.publishEvent(event);
        }
    }
    

    模块 B

    // 模块 B 监听程序
    public class EventListener{
    
        @EventListener
        public void onEvent(){
        	// 同步监听
        }
        
        @Async
        @EventListener
        public void onEvent(){
        	// 异步监听
        }
    }
    
    
    
    第 1 条附言  ·  2022-02-17 09:26:11 +08:00
    关于第一个问题,MQ 确实是一个思路,感谢 @psydonki ,但是利用 MQ 会有事务问题,这个也需要考虑一下。
    14 条回复    2022-02-22 02:50:26 +08:00
    love2075904
        1
    love2075904  
    OP
       2022-02-16 15:28:46 +08:00
    目前针对第一个问题,有一个解决方案是模仿 xxl-job 这种,让模块 B 和模块 C 向模块 A 进行注册,然后通过 http 调用执行
    sky857412
        2
    sky857412  
       2022-02-16 17:55:51 +08:00
    第一个问题,应该将 MessageSender 的实现类都放到一个微服务中,controller 调用这个微服务暴露的接口,具体是调用哪个实现类,在这个暴露的接口中处理
    第二个,应该无法没办法实现同步事件
    wolfie
        3
    wolfie  
       2022-02-16 18:11:05 +08:00
    @FeignClient
    public interface EmailMessageSender extends MessageSender {}
    psydonki
        4
    psydonki  
       2022-02-17 01:10:56 +08:00
    我理解使用 MQ 作为中间件可以很好的解决这个问题。
    Sender 只负责向 MQ 发送消息,同时在 Header 里面注明消息的类型:

    ```json
    {
    "type": "email"
    }
    ```

    然后根据 header 中的类型,将消息分流到不同的 queue 。
    模块 B/C 分别是不同的微服务,监听不同的 queue 就好。
    love2075904
        5
    love2075904  
    OP
       2022-02-17 09:19:06 +08:00
    @sky857412 之前有过这种想法,但是整体看来是不可能的,MessageSender 只是伪代码,实际业务中是不同的业务模块处理代码。
    love2075904
        6
    love2075904  
    OP
       2022-02-17 09:20:45 +08:00
    @wolfie 这种就写死了,但是实际上,模块 A 根本不需要知道有哪些实现,这个时候完全可以出现一个模块 D 来实现一个新的 MessageSender
    love2075904
        7
    love2075904  
    OP
       2022-02-17 09:21:43 +08:00
    @psydonki MQ 确实是一个思路,感谢!
    wolfie
        8
    wolfie  
       2022-02-17 09:44:09 +08:00
    @love2075904 #6
    这个就是放在公共依赖里的 FeignClient 声明。
    你还是可以根据 MessageSender 类型获取所有的 bean 。
    kowgarnett
        9
    kowgarnett  
       2022-02-17 10:41:21 +08:00
    我司的做法是抽出来了一个单独的微服务负责所有的 send message ,需要同步的用 REST call ,不需要的用 Kafka event 处理
    freeup
        10
    freeup  
       2022-02-17 16:55:40 +08:00
    对于拆分单体项目成微服务的我说几个我经历过得坑
    1.方法间调用问题,原来都是直接调用,拆分出去后就是远程调用,所以相关代码都得改造
    2.查询。。查询是最麻烦的,如果都在同一个库都还好,如果拆分时拆分了库。。那么以前的查询要么直接跨库联查,要么单独查
    3.公共部分需要单独打包,也就是一些公共的依赖需要单独新建项目进行开发然后整合,各个服务进行依赖
    4.事务问题,我们用 seata 解决分布式事务问题
    其他的 都是一些业务上的问题了 我遇到过的就这几个问题比较麻烦
    love2075904
        11
    love2075904  
    OP
       2022-02-18 13:40:57 +08:00
    @wolfie 我大概了解您的意思了,感谢。
    love2075904
        12
    love2075904  
    OP
       2022-02-18 13:42:12 +08:00
    @kowgarnett 这也是一个方案,不过目前这样改动影响比较大,这样一来这个单独的微服务可能就不够纯粹,会去调用其他的业务模块代码了。
    love2075904
        13
    love2075904  
    OP
       2022-02-18 13:42:47 +08:00
    @freeup 确实,这几个坑我们肯定也要踩。
    kowgarnett
        14
    kowgarnett  
       2022-02-22 02:50:26 +08:00
    @love2075904 那就看你们对于微服务的业务范围的定义了,这个都是要讨论看 trade-off 找平衡的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3374 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 11:21 · PVG 19:21 · LAX 04:21 · JFK 07:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.