架构的演变过程经历了从单体架构、 SOA 架构、微服务架构,再进入容器化,如今又流行无服务架构,虽然架构在逐步在升级,但是随着业务量的发展会发现版本迭代频率会逐步频繁,随之而引发的代码冲突、模块耦合问题似乎并没有减少。既然都已经是微服务架构了,怎么还是会出现这样的问题呢?架构师们宣导的微服务的优势不是已经解决了这些问题吗?为什么微服务的这些优势在项目中并没有很好的体现出来呢?以下是在实际工作中遇到若干真实案例,我们看看微服务是否真的解决了所面临的问题。
笔者从事的行业是为银行提供金融科技服务,实现银行数字化转型,与银行对接过程中会高频的和银行的内部系统交互的情况,系统与系统之间是通过 ESB 的方式来通讯。架构设计思路为了屏蔽业务系统和 ESB 的耦合,独立了银行网关服务,目的是将 RPC 请求换为 ESB 请求。同时业务系统自身也会暴露 ESB 服务给合作伙伴调用以及行内业务系统异步回调。整体架构如下图所示:
随着项目逐步推进,会发现银行网关代码冲突现象以及 Bug 出现频率明显提高。出现了因一个问题引发其他 bug 的现象,修复 Bug 的效率明显降低,项目进度明显降低,通过深入分析后发现问题的根源出现在代码层面。例如以下代码,设计初衷是针对 ESB 给出不同的 actionCode 做不同的业务处理,而且项目初期接入的系统比较少,简单的 IF 、 ELSE 判断就能解决,随着接入系统逐步变多,整个代码变成不可控局面,相关代码如下。
String notifySystem(String actionCode,String respBody){
if(actionCode.equals(**"A01"**)){
//业务逻辑处理
return **""**;
}else if (actionCode.equals(**"A02"**)) {
//业务逻辑处理
return **""**;
}else if (actionCode.equals(**"A03"**)) {
//业务逻辑处理
return **""**;
} else if (actionCode.equals(**"A04"**)) {
//业务逻辑处理
return **""**;
} else if (actionCode.equals(**"A05"**)) {
//业务逻辑处理
return **""**;
}
return **""**;
}
当项目进入 SIT 阶段时会发现这个通知类已经出现 N 多个分支条件,会有一堆人同时在修改这份代码,代码被覆盖、合并冲突、合并错误等问题高频出现。此时你还敢说你的服务是“微”的吗?那么,为什么采用了微服务架构思想来设计系统,而且服务划分的也很合理,但是在编程阶段“微服务”却变成了“危服务”呢?事实上无论是单体架构还是微服务架构,在进入编码阶段需要关注开闭原则和单一原则这二大原则。
n 开闭原则(对于扩展是开放的,但是对于修改是封闭的):软件实体(模块、类、函数等)应该可以扩展,但是尽量不要做不必要的修改。
n 单一原则(规定一个类应该只有一个发生变化的原因):修改任何类型的分支逻辑代码,都需要只改动当前类的代码。
上述的例子其实就是一个非常典型的多分支处理场景,尽管使用微服务架构,而且设计了合理的服务拆分策略,但是并不能解决多分支判断的情况。在开发过程中要求架构师或者技术经理能经常性的审查代码,一旦发现代码变成不可控局面是,要求研发人员去尽早重构代码,把不可控变成可控。例如本例则可以通过策略方式来实现多分支的场景条件判断,实现对修改关闭,对扩展开放的设计思路。所谓策略模式简单说分二步:
1 、定义一个接口,用来定义需要具体处理的方法
2 、定义一个 Map 来存放具体策略对象,其中 key 是对应 actionCode , value 是具体的实现类
策略模式实现方式如下
1 、先定义接口 ESBHandler ,用来约束策略需要实现的方法
public interface ESBHandler {
String handlerESBRequest (String ebsxml) ;
String actionCode () ;
}
2 、定义策略的具体内容,假设支付通知的 actionCode=A02 ,定义一个支付通知处理类 PayNoticeHandler
@Service
public class PayNoticeHandler implements ESBHandler {
@Override
public String handlerESBRequest (String ebsxml) {
return " 支持通知结果处理 " ;
}
@Override
public String actionCode () {
return "A02" ;
}
}
假设用户额度的 actionCode=A01 ,定义一个额度变更处理类 UserQuotaHandler
@Service
public class UserQuotaHandler implements ESBHandler {
@Override
public String handlerESBRequest (String ebsxml) {
return " 用户额度变动通知处理 :" +ebsxml ;
}
@Override
public String actionCode () {
return "A01" ;
}
}
3 、目前框架已经搭建完成,还需要定义一个 Map 用来存放 acionCode 和具体类的映射关系,因此再定义一个 HandlerContext ,用来存放映射关系,相关代码如下:
public class HandlerContext {
private Map handlerMap = null;
public HandlerContext (Map handlerMap){
this . handlerMap = handlerMap ;
}
public ESBHandler getHandler (String type){
if (type == null ) {
throw new BusinessException( 101 , "type 参数不能为空 " ) ;
}
ESBHandler clazz = handlerMap .get(type) ;
if (clazz == null ) {
throw new BusinessException( 9999 , " 该类型没有定义,请定义: " + type) ;
}
return clazz ;
}
}
最后需要查询 ESBHandler 接口所有的实现类,并将 actionCode 和具体类的映射关系放入 HandlerContext 。因此再定义 HandlerProccessor ,
@Component
public class HandlerProccessor implements BeanFactoryPostProcessor {
_/**
*__获取接口 ESBHandler 的所有实现类,初始化 HandlerContext ,将其注册到 spring 容器
*
* **@param**__beanFactory__bean__工厂
* **@throws**BeansException
*/_ @Override
public void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory) throws BeansException {
Map handlerMap = new HashMap<>() ;
Map beanMaps = beanFactory.getBeansOfType(ESBHandler. class ) ;
beanMaps.forEach((name , bean) -> {
handlerMap .put(bean.actionCode() , bean) ;
}) ;
HandlerContext context = new HandlerContext(handlerMap) ;
// 单例方式注入到 Spring 容器,目的是统一管控
beanFactory.registerSingleton(HandlerContext. class .getName() , context) ;
}
}
最后,策略模式使用方式就比较简单了,首先解析 esb 的报文获取 actionCode ,其次根据 actionCode 查询对应的处理类,最后再调用具体的策略类。
@Autowired
HandlerContext holderContext ;
public String getAction ( String esbxml) {
// 解析 esb 报文,获取 actionCode
String actionCode=getActonCode(esbxml) ;
return holderContext .getHandler(actionCode).handlerESBRequest(esbxml) ;
}
策略模式减少了各种分支条件的判断,降低的程序的复杂度。那么在实际开发过程中是否遇到了有分支条件的就必须使用策略模式呢?这里没有确定的答案,需要在编码前有对当前流程以及后续扩展有预判。但是有一点可以肯定的是在编码过程中使用设计模式,有利于架构的清晰以及提高程序员的编码水平。
列举一个用户登录的场景,用户使用验证码登录 APP ,后端需要校验验证码是否有效、是否是黑名单用户、是否是常用设备 ……. 等非常多的前置条件校验 , 只有当所有条件都满足后才完成登录,而且对于每项检查的先后顺序也有要求。下面的代码是开发人员的具体实现方式,从功能实现角度来看这些代码完全满足业务要求,但是从可维护性角度来看,任何逻辑的变动都需要修改 LoginService 这个类,不满足“开闭原则”的代码要求。
public class LoginService {
public String userLogin (String loginRequest){
checkVerifyCode(loginRequest) ;
checkIsBlackUser(loginRequest) ;
checkCommonEquipment(loginRequest) ;
// 登录逻辑处理
return "login_success" ;
}
_/**
*__校验验证码是否正确
* **@param**__loginRequest__*/_ public void checkVerifyCode (String loginRequest){
// 业务逻辑处理
throw new BusinessException( 1001 , " 验证码错误 , 请重新输入 " ) ;
}
_/**
*__判断用户是否是黑名单用户
* **@param**__loginRequest__*/_ public void checkIsBlackUser (String loginRequest){
// 业务逻辑处理
throw new BusinessException( 1002 , " 用户身份异常 , 登录失败 !" ) ;
}
_/**
*__常用设备登录 , 若非常用设备则需要做身份核验
* **@param**__loginRequest__*/_ public void checkCommonEquipment (String loginRequest){
// 业务逻辑处理
throw new BusinessException( 1003 , " 非常用设备,需要先执行身份核验! " ) ;
}
}
面对这种需求和如此风格的代码,到底该不该去重构以及如何重构,这是考察一个技术管理者的一道命题作文。重构短期内看不到效果,而且会增加人力投入,不重构则是存在的一个隐性风险。在此也是建议每个技术人员应该都有一份“极客”精神,将风险和问题尽早暴露尽早修复。面对如此多的判断条件,当然是使用设计模式才能让代码更优,而且在编码设计过程中同样需要遵循开闭原则和单一原则,本需求中开闭原则指增加新的判断逻辑通过扩展来实现,而不用修改 LoginService ,单一原则是指每个扩展的类只处理本身相关的逻辑。为此可以考虑使用责任链模式来重构这段代码,所有关联的检查条件都使用“链”的方式来串联起来,每个“链”上的节点只完成独立的事件,任何规则的调整都不会影响登录主流程。责任链模式简单来说分以下几个步骤:
1、 创建抽象类,并定义抽象方法 , 在抽象类中以递归调用的方式来实现节点和节点之间首尾连接
2、 创建子类并实现抽象类中的方法
3、 创建链的初始化类,指定上下游对应链的关系
具体方式方式如下,首先创建抽象类 AbstractCheckChain
public abstract class AbstractCheckChain {
private AbstractCheckChain nextChain ;
_/**
*__责任链的下一个对象
*/_ public void setNextChain (AbstractCheckChain nextChain){
this . nextChain = nextChain ;
}
public AbstractCheckChain getNextChain () {
return nextChain ;
}
_/**
*__核心点,递归调用
* **@param**__loginRequest__*/_ public void doCheck (LoginRequest loginRequest){
execCheck(loginRequest) ;
if (getNextChain() != null ) {
getNextChain().doCheck(loginRequest) ;
}
return ;
}
public abstract void execCheck (LoginRequest loginRequest) ;
}
其次,创建相关子类例如验证码校验,完成相关业务逻辑。
@Component
@Order ( 1 )
public class CheckVerifyCodeChain extends AbstractCheckChain {
@Override
public void execCheck (LoginRequest loginRequest) {
System. _out_ .println( " 验证码校验 " ) ;
}
}
创建子类(黑名单校验)
@Component
@Order ( 2 )
@Slf4j
public class CheckBlackUserChain extends AbstractCheckChain {
@Override
public void execCheck (LoginRequest loginRequest) {
System. _out_ .println( " 黑名单检查 " ) ;
}
}
最后,初始化链路,各节点首尾相连接,实现“链”。
@Component
public class LoginCheckService {
@Autowired
private List abstractCheckChainList ;
private AbstractCheckChain firstChain ;
@PostConstruct
private void initializeCheckChain (){
for ( int i = 0 ; i< abstractCheckChainList .size() ; i++){
if (i == 0 ){
firstChain = abstractCheckChainList .get( 0 ) ;
} else {
AbstractCheckChain currentHander = abstractCheckChainList .get(i - 1 ) ;
AbstractCheckChain nextHander = abstractCheckChainList .get(i) ;
currentHander.setNextChain(nextHander) ;
}
}
}
public String check (LoginRequest loginRequest) {
// 登录前置判断
firstChain .doCheck(loginRequest) ;
// 登录后的逻辑处理
return " 登录成功 " ;
}
}
用户登录接口在执行登录前先执行前置 LoginCheckService#check 方法即可。通过责任链模式重构后代码结构非常清晰,各节点的责任明确,假设需要修改黑名单功能,则只需要调整 CheckBlackUserChain 即可,对主流程无影响。如要调整链执行的前后顺序,则只需调整 @Order() 接口对应的值即可。若后续扩展新的校验规则则直接继承抽象类 AbstractCheckChain 即可。
服务拆分后分为用户服务、产品服务、订单服务等各种服务,后台管理系统查询订单后显示需要用户姓名、手机号码、注册时间、产品名称、价格、购买数量,这就涉及到多表关联查询,但是对应的数据都存在在各自独立的数据库,传统的 join 方式关联查询模式已经不能解决这种需求。正确处理的流程如下
1、 获取当前分页记录订单记录表
2、 当前记录根据用户 ID 去重,获取当前页的所有用户
3、 根据当前页用户 ID 的集合批量查询用户信息
4、 当前记录根据产品 ID 去重,获取当前页的所有产品
5、 根据当前页产品 ID 的集合批量查询产品信息
6、 将查询结果组织成最终输出的对象
具体代码实现方式很多中,在本例中我们仍然采用责任链模式,将查询出来的每个对象都丢到链中,各节点负责对链上对象进行赋值。相关代码如下
1、 定义抽象类 AbstractOrderConvertChain ,定义需要转换的方法 convert 以及递归方法 doConvert
public abstract class AbstractOrderConvertChain {
private AbstractOrderConvertChain nextChain ;
_/**
*__责任链的下一个对象
*/_ public void setNextChain (AbstractOrderConvertChain nextChain){
this . nextChain = nextChain ;
}
public AbstractOrderConvertChain getNextChain () {
return nextChain ;
}
public void doConvert (UserOrderDTO userOrderDTO , UserOrderPO userOrderPO , ChainContext context){
convert(userOrderDTO , userOrderPO , context) ;
if ( this .getNextChain()!= null ){
this .getNextChain().doConvert(userOrderDTO , userOrderPO , context) ;
}
}
public abstract void convert ( UserOrderDTO userOrderDTO , UserOrderPO userOrderPO , ChainContext context) ;
}
2、 定义用户信息转化类 UserInfoConvert
@Component
public class UserInfoConvert extends AbstractOrderConvertChain {
@Override
public void convert (UserOrderDTO userOrderDTO , UserOrderPO userOrderPO , ChainContext context) {
Map userMap = context.getUserPOList().stream().collect(Collectors._toMap_(UserPO::getUserID , Function._identity_() , (key1 , key2) -> key1)) ;
userOrderDTO.setUserName(userMap.get(userOrderPO.getUserID()).getUserName()) ;
userOrderDTO.setMobilePhone(userMap.get(userOrderPO.getUserID()).getMobilePhone()) ;
}
}
3、 定义产品信息转化率 ProductInfoConvert
@Component
public class ProductInfoConvert extends AbstractOrderConvertChain {
@Override
public void convert (UserOrderDTO userOrderDTO , UserOrderPO userOrderPO , ChainContext context) {
Map productMap = context. getProductPOList().stream ().collect(Collectors._toMap_(ProductPO::getId , Function._identity_() , (key1 , key2) -> key1)) ;
userOrderDTO.setProdctName(productMap.get(userOrderPO.getProductID())== null ? " 未知 " :productMap.get(userOrderPO.getProductID()).getProductName()) ;
}
}
4、 定义链的初始化类 UserOrderConvertChainService
@Component
public class UserOrderConvertChainService {
@Autowired
private List abstractOrderConvertChainList ;
private AbstractOrderConvertChain firstChain ;
@PostConstruct
private void initializeCheckChain (){
for ( int i = 0 ; i< abstractOrderConvertChainList .size() ; i++){
if (i == 0 ){
firstChain = abstractOrderConvertChainList .get( 0 ) ;
} else {
AbstractOrderConvertChain currentHander = abstractOrderConvertChainList .get(i - 1 ) ;
AbstractOrderConvertChain nextHander = abstractOrderConvertChainList .get(i) ;
currentHander.setNextChain(nextHander) ;
}
}
}
public void doConvert (UserOrderDTO userOrderDTO , UserOrderPO userOrderPO , ChainContext context){
firstChain .doConvert(userOrderDTO , userOrderPO , context) ;
}
}
最后 在使用前需要先组织好查询的对象 , 为了方便扩展,定义了 ChainContext 类处理上下午关系。
public String searchUserOrderList (){
List searcResList = getUserOrderList() ;
List userOrderDTOS = new ArrayList<>() ;
ChainContext context = new ChainContext() ;
context.setProductPOList(builderProduct( null )) ;
context.setUserPOList(builderUser( null )) ;
List userOrderDTOList = searcResList.stream().map( userOrderPO -> {
UserOrderDTO userOrderDTO = new UserOrderDTO() ;
userOrderConvertChainService .doConvert(userOrderDTO , userOrderPO , context ) ;
return userOrderDTO ;
}).collect(Collectors._toList_()) ;
System. _out_ .println(userOrderDTOList) ;
return "" ;
}
这种分库分表后的聚合查询适合查询频率不高,查询维度简单的场景,如果查询条件复杂,查询频率高,那这种方法就不合适。
在微服务架构下,通常使用 MQ (消息队列)来解决服务之间的相互依赖,但是使用了 MQ 后业务一定能按预期完成完整的业务流吗?在此列举金融项目中用户在线申请借款的一个业务场景。用户通过 APP 或者微信小程序在线借款时需要用户授权在线签署合同,在线签署合同会调用第三方有资质的签章公司,最终会形成一份 PDF 格式的合同。因为签章比较耗时,因此通过发送 MQ 消息方式驱动签章服务完成签章,并更新签章状态。相关伪代码如下
public void signatureLoanApply1 (String orderDTO){
// 先发送 MQ 消息,发送成功后写入签章事件
if (sendMQ(orderDTO)){
// 写入签章事件
insertSignature(orderDTO) ;
}
return ;
}
这段代码从架构角度来看,使用了 MQ 来解耦执行耗时的事件。符合期望,但是在具体使用过程中却出现了问题。假设 MQ 发送失败了,签章事件就不能正常写入,这个对业务来说是不允许出现的异常。因此工程师又把流程重新调整,先插入数据,再发起签章流程,相关代码如下:
public void signatureLoanApply2 (String orderDTO){
if (insertSignature(orderDTO)){
sendMQ(orderDTO) ;
}
return ;
}
先写入签章事件,写入成功后再发 MQ 消息。综合考虑方案 2 比方案一要好,但是方案 2 还是缺少了消息补偿机制。若签章服务收到 MQ 消息后调用第三方服务执行签章失败,若已达到 MQ 的重试次数,那么这个消息就进入死信队列。因此需要使用定时任务去定期扫描未完成签章的事件,再次发送 MQ 消息。最终在项目中定下一条铁律“使用 MQ 的场景一定需要有补偿机制来确认最终结果”。
分布式架构下因数据不一致性所引发的问题会逐步增多,稍有不慎就会出现系统之间的异常导致各种问题出现,大多数情况下研发人员在编码过程中都不会发现潜在的风险。
笔者所在研发团队曾经为未某银行开发了一套存款理财系统,其流程是用户先在 APP 端绑定银行卡并开通电子账户并将钱转入电子账户,当用户购买存款产品后调用存款服务接口(存款服务由第三方公司开发)发起理财购买请求,存款服务完成扣款等一系列问题后返回购买结果 , 相关伪代码如下。
@Transactional
public void buy(String buyRequest){
//调用esb服务发起购买申请
if(esb.buyProduct(buyRequest)){
service.insert(....) //生成购买订单
//其他业务处理
}else{
throw BusinessException (2001,**"****购买存款产品失败!"**)
}
}
俗话说“不怕程序员写的代码出现 Bug 而是怕程序员在写 Bug 的时候自己居然没有察觉”,上述代码运行正常的前置条件是“当前服务本身以及关联服务都正常运行”。实际上在服务运行过程中会出现以下问题:
情况 1 :假设执行 esb.buyProduct(buyRequest) 远程调用时第三方服务响应慢导致接口超时,会导致存款服务扣款成功但是因接口超时导致业务系统没有生成订单购买记录。
情况 2: 假设执行 esb.buyProduct(buyRequest) 返回成功,即将执行 service.insert(....) 生成购买订单时因发版导致服务重启,仍然会出现被扣款但是没有看到订单的情况。
在分布式架构下,解决数据不一致的问题通常需要知晓 2 个概念即流水号和补偿机制,系统之间的每次交互必须有一个唯一的流水号,可以根据流水号查询当前交易的状态。补偿机制即后台有定时任务来驱动状态更新。为次,当前购买流程可以调整如下:
1、 先把交易订单入库,交易状态为初始状态,同时交易流水号也需要写入表
2、 调用第三方购买服务,若第三方服务正常返回状态这更新当前交易状态
3、 若第三方超时或者第三方有返回结果但是更新状态失败,则通过补偿机制来更新状态
调整后的代码如下:
public void buy(String buyRequest){
//调用esb服务发起购买申请
service.insert(....) //生成购买订单,交易状态默认为初始状态=0
boolean status = esb.buyProduct(buyRequest)){
service.update(……) //修改交易状态
//其他业务处理
}
同时新增定时任务扫描所有 N 分钟前的订单状态为初始状态 =0 的订单,并根据流水号来查询交易状态。
无论是单体架构,还是微服务架构或者无服务架构,只是一种架构思路,然而面对实际的业务问题 还需要结合设计模式综合考虑,因为设计模式是一种代码规范,也是在面向对象开发过程中的最佳实战,当面对许多不同情况下时可将设计模式作为处理的问题模板。当然使用设计模式时需要结婚团队自身技术情况。编码之前首先需要深入理解业务,做好业务预判,特别是面对复杂的业务场景以及频繁迭代的某些业务功能,此时就需要考虑是否进行代码重构来解决。另外,在实现业务功能时候,要多思考异常情况而不是仅完成了正常流程。必须要将异常和风险扼杀在上线前,再合理使用设计模式,只有这样你设计的服务才是“微”服务。
如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!
赞3
添加新评论3 条评论
2021-12-29 23:59
2021-12-29 09:22
2021-12-27 10:07