对于工作年限不长的程序员来说,知识储备是非常关键的。在开发中各种技术的应用已经非常普遍了,例如常见的各种ORM,各种中间件如Redis,MQ等等,又如WebApi路由配置等等,对于常常做开发的程序员来说,都是小事,我们就从小事说起。
在微服务中,常用到MQ作为微服务之间通讯的桥梁,以RabbitMQ为例,其转发的方式有direct、fanout、topic,而在微服务中的应用,几乎不会使用direct这种交换模式。对于程序员来说,具体使用哪一种交换模式,需要开发时候双方去协商和沟通通讯的数据结构,还要处理通讯过程出现异常的情况,这样下来,从发送和校验到接收和处理,一系列下来,代码少说也有一百几十行,还要话费沟通成本,还需要程序员对mq的掌握。对于组织开发者来说,就是一大堆的培训工作,不培训就要花费时间成本。
fanout这种交换模式,就如大喇叭,发出去的消息,只要愿意,都能够接收到,是一种比较开放的消息发布模式。例如一个员工的基础信息发生了改变,其他微服务需要更新这个员工的显示信息,那么都来接收就好了,至于接收到怎么处理,那是接收方的事情了,只要接收成功,回应一个ack信号就可以了。
topic这种交换模式,是具有指定性的,只有匹配到topic字符串的接收方,才能收到消息,就向路由一样。例如做一个系统的内部通知消息,要通知到岗位或者个人,可以使用通配符:
匹配多个多个单词,例如message.pos1.emp1 使用message.# 所有消息都能成功匹配上;
* 匹配单个单词,例如message.pos1.* 在pos1下的所有人都能收到这个消息。
程序员是否需要了解这些?我感觉不需要,我们把发送消息封装成一组类,一个是用于发送,一个是用于接收,例如:
生产者接口
public interface MQProducer
{
void Publish<T>(T body);
}
消费者接口
public interface MQConsumer
{
//使用topic订阅消息
void Consume(QueueDeclare queue, string topic,Action<string> action);
//使用fanout订阅消息
void Consume(QueueDeclare queue,Action<string> action);
}
上述方法,只要稍微了解MQ的,都知道这么封装,在微服务中的消息传递相对比较简单的,可以进一步封装。生产者这一端,可以使用AOP方法制作一个发布消息的标签,例如:
public class SaleSerivce : ServiceBase<SaleHdr,SaleDtl>
{
//当成功保存单据后,当前微服务ID为AppId,以Sale为资源,SaveBill为action标记发送SaleHdr对象
[MQPublish]
public RValue<SaleHdr> SaveBill(SaleHdr hdr,List<SaleDtl> dtls)
{
}
//这里解释一下RValue<T>这个结构
//当方法体出现异常,RValue<T>接收Exception对象,并设置Success=false;
//当方法体返回字符串,RValue<T>.Message接收字符串值,并设置Success=false;
//当方法体返回SaleHdr对象,RValue<T>.Value接收对象,并设置Success=true;
//MQPublish标签,在Success==true时候把SaleHdr对象包装成一个标准的通讯数据格式后,转成json发送出去
}
接收端就有点复杂了,对不同交换方式,要开发一套对应的数据接收和转发机制。 首先,接收到的消息,否存到一个消息对象中,然后塞到一个Queue对象中,只要向Queue中加元素,则触发出队列的方法,直到读取Queue完成才终止。
public class PurService : SeriveBase<PurHdr,PurDtl>
{
[MQConsume("saleApp","sale","savebill")] //appid,resource,action
public RValue<PurHdr> MQSaleSave(SaleHdr saleHdr)
{
//当新建销售单的时候,采购查询一下是否需要补货
}
}
建立一个MQStarter来订阅MQ消息,并根据MQConsume标签上的参数,匹配到方法,可能会匹配到多个方法,没有关系,通通调用一次就可以了。如果RValue<PurHdr>.Success==false ,则做日志,并把消息方到另外一个容器中,允许重试规定次数后再手动重发。
上述生产者和订阅者,都没有接触到MQ的具体对象和相关代码,只用了两个标签就完成了两个微服务之间的数据交互,只要框架的开发者能够在这个封装中充分考虑到各种情况和处理好各种异常,那么对于程序员来讲是非常轻松的事情,不用考虑通讯协议、结构、方法等等,集中精力编写业务逻辑代码。即使出现通讯问题,只需要反馈就可以了,让开发回归到本质。
在微服务中,用得最多的就是httpClient、redis这两个组件。
HttpClient可以根据封装好的WebApi标准接口进行封装,程序员只需要直接调用方法和给参数就可以完成api调用且方便处理好api返回值,转换成所需要的数据结构。
Redis同理,可以封装成MemoryCache这样的操作方式,分别多String,Hash等数据结构操作。
为了然程序员最大程度减少对技术依赖,把一些常用或者复杂的工作进行封装,转化为简单的操作方式,例如ORM,特别是使用EF的,往往需要程序员开发时候直接操作EF读写数据,带来的问题,往往没有处理读写时候的异常,导致程序在特定情况下卡死或者报错。这里建议把ORM用一层数据访问层包起来,而这个ORM对象只是一个protected层级的,把常规的读写操作都封装成方法,派生出来的对象,都是使用方法,而不使用ORM对象。即使那天把ORM换掉了,对原来的程序也没有一点影响,例如原来程序使用mysql,后面发现结构很简单,用mongo可能更合适,只要改变一下数据层的实现类就可以了,一般数据层都是注入到系统的,对于部署来说,就只是换了一个dll文件,对于程序员来说,几乎没有影响,除非操作了ORM。
程序员连最基本的数据库操作对象都不需要了解,甚至可以不知道用了哪种ORM,程序员就纯粹写业务逻辑,可以把业务中的各种复杂情况都考虑仔细。让程序开发真正回归简单化。
这样带来一个问题,非常不利于程序员的成长,这个就需要程序员在工作过程中注意细节、更多思考,从别人的案例中学习技术技巧,多尝试,转变成真正自己的知识。
在我的框架中,把事务处理也封装成一个标签,这个是参照java中的写法,只是加了更多自己的想法,把事务嵌套、分布式事务都考虑进去了,例如:
[Transaction] //事务标签,若当前没有在事务中,则发起事务,否则这个标签相当于透明
publice RValue<StoreInHdr> SaveStoreIn(StoreHdr,List<StoreDtl> dtls)
{
//这里要处理进仓的操作,同时发起分布式事务标记到货单和采购单
//完成库存的更新
}
这样让程序员完全不用考虑事务过程的处理,至于是提交还是回滚,那就看RValue的返回值。
对于企业来说,用人是一个很大的风险,找水平高的,不愿意带人,写的代码一般程序员还不一定能看懂,自己还有自己的一套风格,换个人维护就很难了。找水平低的,又不按规则来,考虑问题不仔细,会出很多乱子。通过封装,除了简化了开发难度,也制定了开发规则,使用统一标准,有利于项目的持久迭代和发展。