从单体到SpringBoot/SpringCloud微服务架构无感升级的最佳实践

news2024/11/11 4:47:55

目录导读

  • 从单体到SpringBoot/SpringCloud微服务架构无感升级的最佳实践
    • 1. 业务背景
    • 2. 当前问题
    • 3. 升级方案
    • 3.1 架构设计
    • 4. 详细设计
      • 4.1 迁移阻碍
      • 4.2 解决思路
    • 5. 实现过程
      • 5.1 认证兼容改造
      • 5.2 抽象业务流程
        • 5.2.1 抽象业务的思路
        • 5.2.2 抽象业务的抽象编码
        • 5.2.3 抽象业务的具体实现
      • 5.3 错误码兼容处理
      • 5.4 接口兼容适配
    • 6. 思考总结
    • 7. 参考资料

从单体到SpringBoot/SpringCloud微服务架构无感升级的最佳实践

1. 业务背景

  • 有一个单体的Python接口服务平台,提供了认证接口、多种图片业务、多种视频业务等,除了认证接口外,各个业务之间是独立的;
  • 单体的Python接口服务平台中,有一个图片业务是有状态的,且其并发量较高,占用内存较多,并同时需要占用大量的Redis缓存;

2. 当前问题

  • 多种业务代码耦合在一起,牵一发而动全身,开发维护效率非常低,且多次因为小需求上线差点导致了生产问题;
  • 上述有状态的图片业务高峰期时,导致单个进程的python协程处于拥塞状态,横向扩容增加实例也无法解决,且该问题导致了其他所有业务受影响;
  • Python接口服务平台中的认证方式不合理,其采用了类似AmazonS3的AK/SK认证 方式,但是二者的使用场景却并不一样。AmazonS3的AK/SK认证仅需要知道客户是否有访问该S3(即对象存储,一般也叫OBS)的桶的权限,不涉及其他资源的访问控制;而我们实际业务是有非常多的接口,AK/SK认证通过了,仅表示可以合法地访问我们的系统,但是却无法做到精确访问具体的哪几个接口;

3. 升级方案

  • 经过分析评估,架构升级迫在眉睫,尤其是需要往服务拆解、隔离的方向去做新的技术架构;
  • 当下比较好的方向是Java技术栈和Go技术栈,基于团队技能等综合因素考虑,最终选择了Java SpringBoot/SpringCloud微服务技术栈;
  • 在新技术架构的平台上,设计一套更规范合理的接口服务,同时在此基础上,再适配一套兼容的旧接口服务,和以前完全保持一致;
  • 通过Python接口服务平台前置的Nginx集群,按照权重(客户流量的百分比)逐步灰度切换至新平台,待新平台稳定后,逐步放量直至全部切换过来;

3.1 架构设计

  • 结合实际业务场景,需要提供新的认证方式,需要做到精确到接口的访问权限控制,同时还要把认证逻辑从业务逻辑中剥离出来;
  • 为了提升认证效率,需要做到认证和鉴权的分离,避免频繁地查询接口或者数据库;
  • 经过调研,通过扩展Jwt Token(Java Web Token)字段,可以非常容易做到精确的接口访问权限控制;
  • 采用wt Token后,Token校验和生成完全可以放在不同的服务中去完成了。Token校验采用基于SpringBoot-WebFlux的SpringCloud-Gateway,Token生成服务采用了基于SpringSecurity的Spring-Authorization-Server;
  • 除了提供新的认证方式,还必须要兼容原来的AK/SK认证认证接口,同时也要在接口中新增接口访问权限的校验;
  • 鉴于平台还需要和多个外部系统对接,单独设计了一个基于插件的对接服务。对接服务可以通过插件包扩展的方式,支持越来越多的三方对接,同时完全不会影响到其他业务;
  • 综合上面的分析,逻辑架构设计如下:
    bq逻辑架构

    逻辑架构补充说明如下:

    • 网关服务(bq-gateway) :统一的接入网关,主要负责安全鉴权,即JwtToken的合法性校验和接口方案权限的校验;同时,所有外部调用也需要通过本网关服务
    • 认证服务(bq-auth) : 负责生成JwtToken,同时对网关服务提供访问权限的查询接口,且只读数据库数据;
    • 业务服务(bq-biz) : 负责实现具体的业务逻辑,且只读数据库数据;
    • 对接服务(bq-integration) : 负责和外部平台对接,通过网关服务中转,且只读数据库数据;
    • 后台管理(bq-mgr) : 负责维护接口平台,严格来讲不属于微服务集群,也不对外暴露,仅通过数据持久层异步通知认证服务业务服务对接服务其数据变更并生效;
    • 网关服务认证服务实现了鉴权和认证的分离;
    • 后台管理认证服务业务服务对接服务实现了数据库的读写分离;
    • 以上涉及主要考虑接口平台的场景,当扩展支持Web界面时,需要在网关服务认证服务做认证和鉴权的升级改造;
  • 在逻辑架构设计的基础上,进一步完善了中间件依赖,其部署架构图如下: bq部署架构图
    • 注:在实际场景中,其实使用的是K8S自带的服务注册和服务发现,为了简化场景,暂忽略K8S容器,使用了Nacos作为服务注册中心;

    部署架构补充说明如下:

    • NacosServer:负责服务注册和服务发现,同时也是Sentinel熔断限流和网关服务路由的配置中心;
    • Sentinel:负责熔断降级和非业务的限流,同时支持基于SpringBoot-WebFlux的网关服务和基于SpringBoot-Web的其他服务;
    • ZipkinServer:负责链路追踪汇总查询,同时支持基于SpringBoot-WebFlux的网关服务和基于SpringBoot-Web的其他服务;
    • 为了后续能够平滑切换至新SpringCloud架构的组件,当然也包括切换至k8s,当下都是极简使用NacosServerZipkinServer
    • 上述架构已全部开源,参见Java开源接口微服务代码框架文档详细内容了;

    4. 详细设计

  • 接口服务平台虽然业务较多,但是其业务场景及其操作步骤基本类似,因此可以考虑对业务场景进行抽象提炼,尽量编写少的差异代码逻辑,复用公共抽象逻辑;
  • 由于业务需要和非常多的三方服务对接,安全认证方案各个不同,因此在分析总结老Python系统的基础上,结合以前积累的代码库,构建的代码架构设计如下:
    bq代码架构图

4.1 迁移阻碍

  • 上述的架构设计基本上解决了面临的几个核心问题,但是还是无法做到系统的无感迁移,原因如下:
    • Python老平台的错误码设计不合理,部分接口透传了第三方的错误码信息,部分又是自定义的错误码信息,且自定义的错误码里面还存在一码对多个Message的情况。当下根本无法对照代码、文档甚至是日志分析出所有错误码的场景;
    • Python老平台的接口设计不合理,Json报文中,有的字段为空时,是空字符串,但是有值时又是子Json对象;相同含义的同一字段,有的接口是子Json数组,有的接口却又是Json对象……情况千奇百怪;

4.2 解决思路

  • 针对错误码阻碍,想到的措施如下:
    • 错误码可变性较大,会随着业务场景变化而变化(一般只增不减),所以需要把所有错误码放到独立的配置中,与代码隔离;
    • 从业务出发,搜集近一年的生产脱敏数据进行分析,提炼出每个接口的可能错误码和对应Message;
    • 在梳理汇总错误码的基础上,做剪枝操作(即一个错误码只保留一个Message),并跟业务团队共识都认可的方案;
    • 把错误码分三层,内层是和三方服务对接的错误码,映射了三方服务和标准错误码的关系;中层为标准错误码,记录了新接口服务平台的所有场景错误码;外层是兼容错误码,映射了标准错误码和兼容错误码的关系,兼容错误码也就是上条所说的剪枝并共识的错误码;
  • 针对接口字段不规范,选用扩展性更强的格式作为统一标准,在兼容接口上预留扩展点,方便做特殊定制处理;

5. 实现过程

  • 在上述系统分析的前提下,主要分了以下几个方面来对接口平台做升级改造。

5.1 认证兼容改造

  • 通过网关服务认证服务做JwtToken的鉴权和认证的分离改造参见OAuth2在开源SpringBoot/SpringCloud微服务框架的最佳实践 ;
  • 兼容AK/SK鉴权逻辑设计如下:
    1. 网关服务处配置一个对接认证服务的特殊AK/SK,收到客户请求时,通过这个特殊的AK/SK先生成一个JwtToken并缓存在本网关服务实例中;
    2. 网关服务JwtToken缓存存在时,调用认证服务,获取客户的真实账号和接口权限信息;JwtToken缓存不存在时,则执行上一步获取;
    3. 比对用户的接口权限信息是否合法,如不合法,则返回签名失败;合法时,则按照AK/SK的签名校验规则进行签名校验;

    由于AK/SK的设计很少出现在接口权限的认证场景中,此处暂略掉其实现;

  • 网关服务认证服务需要支持前端页面认证访问时,则需要做如下的改造设计:
    1. 在前端页面做登录认证时,认证服务需要同时生成JwtToken和刷新Token,前者有效期30分钟,后者1小时;JwtToken只能用作业务接口的调用,刷新Token只能调用刷新接口;
    2. 网关服务处确保JwtToken只用作业务接口的调用、刷新Token只调用刷新接口;
    3. 当JwtToken和刷新Token都过期时,页面要返回到登录界面,重新让用户登录;
    4. 当JwtToken过期而刷新Token未过期时,则由客户端使用刷新Token发起Token刷新调用,刷新成功后,使用新的JwtToken做业务调用;
    5. 由于带有前端页面的系统,通常角色、权限、菜单配置较多,其认证信息已不合适放在JwtToken字段中,网关服务认证服务可以通过集群共享Redis来实现;即:
      1. 认证服务生成JwtToken时,同时把JwtToken信息缓存至与网关服务共享的Redis。网关服务仅连接高性能的Redis几乎不影响性能;
      2. 网关服务在校验JwtToken签名合法时,同时还要根据JwtToken获取共享Redis中的相关权限信息,只有权限匹配通过时,才能继续调用业务接口;否则返回认证失败;
      3. 当用户的认证信息发生变更时,认证服务清理掉Redis缓存,下一次网关服务比对缓存时,就能够及时感知到认证信息的变化;
      4. 认证服务需要使用spring-data-redis做分布式缓存;

    当下开源框架已实现了前面2步;

5.2 抽象业务流程

5.2.1 抽象业务的思路

  • 因为业务相似度比较高,所以考虑使用泛型对业务流程进行抽象,包括RestController/Service/RemoteService层。
  • 业务抽象的核心是对业务模型进行抽象。步骤如下:
    • 定义了RestController的基类,RestController的入参抽象为泛型<I>,出参模型为<O>,中间服务层Service处理的标准模型为<T>;
    • RestController接口的每个<I>模型都可以直接转换成<T>模型;
    • 定义了Service/RemoteService基类,Service/RemoteService在业务处理过程中,都只使用<T>模型,Service/RemoteService获取的结果需要先适配成<O>模型;
    • 抽象的RestController自动注入抽象的Service,抽象的Service按照实际业务需求注入抽象的RemoteService;

5.2.2 抽象业务的抽象编码

  • 上面的说法基于泛型和抽象类,确实有点抽象,下面直接上代码:
    • 定义RestController基类:
      @Slf4j
      public class BaseBizController<O, T extends BaseBiz<O>, I extends BaseBizInner<T>>
      {
          /**
           * 获取批量结果(适用于接口调用)
           *
           * @param inner 入参业务模型
           * @return 批量出参模型
           */
          @ClientLogAnn
          public ResultCode<List<O>> batchExecute(I inner)
          {
              return getService().batchExecute(inner.toModel());
          }
      
          /**
           * 获取单个结果(适用于接口调用)
           *
           * @param inner 业务入参模型
           * @return 业务结果模型
           */
          @ClientLogAnn
          public ResultCode<O> execute(I inner)
          {
              return getService().execute(inner.toModel());
          }
      
          /**
           * 获取服务(支持覆写)
           *
           * @return 注入的服务对象
           */
          protected RestService<O, T> getService()
          {
              return service;
          }
      
          /**
           * 注入标准的业务服务
           */
          @Resource(name = BootConst.DEFAULT_REST_SVC)
          private RestService<O, T> service;
      }
      
      • 按照上面的做法统一定义RestController后,就可以统一使用Redis对继承于BaseBiz(<T extends BaseBiz>)的标准模型做统一的Redis业务限流了;
      • RedisRedis业务限流逻辑参见:熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践 说明;
    • 定义了抽象的Service基类RestService:
      @Slf4j
      public abstract class BaseRestService<O, T extends BaseBiz<O>> implements RestService<O, T>
      {
          @Override
          public ResultCode<List<O>> batchExecute(T model)
          {
              if (null == model)
              {
                  log.error("failed to get valid parameter.");
                  return ResultCode.error(ErrCodeEnum.VALID_ERROR.getCode());
              }
      
              //1.添加客户/接口定制化的参数(比如业务阈值等)
              this.appendConfig(model);
      
              //2.发起远程调用
              return this.invokeBatchResult(model);
          }
      
          @Override
          public ResultCode<O> execute(T model)
          {
              if (null == model)
              {
                  log.error("failed to get valid parameter.");
                  return ResultCode.error(ErrCodeEnum.VALID_ERROR.getCode());
              }
      
              //1.添加客户/接口定制化的参数(比如业务阈值等)
              this.appendConfig(model);
      
              //2.发起远程调用
              return this.invokeResult(model);
          }
      
          /**
           * 业务模型丰富上接口级的全局配置参数
           *
           * @param model 业务模型
           */
          protected void appendConfig(T model)
          {
              //1.查询出urlId
              Map<String, String> urls = MapUtils.invertMap(assemblyConfService.getClientUrl());
              String urlId = urls.get(model.getUrl());
              if (!StringUtils.isEmpty(urlId))
              {
                  model.setUrlId(urlId);
              }
      
              //2.添加客户/接口定制化的参数(比如业务阈值等)
              GlobalConfig config = new GlobalConfig();
              config.setClientId(model.getUserId());
              config.setUrlId(model.getUrlId());
              List<GlobalConfig> configResults = assemblyConfService.getChannelConf(config);
              model.appendConf(configResults);
          }
      
          /**
           * 获取远程调用的结果(根据业务情况去覆写,可以不需要remote服务调用)
           *
           * @param model 业务模型
           */
          protected ResultCode<O> invokeResult(T model)
          {
              ResultCode<O> resultCode = ResultCode.error(ErrCodeEnum.SERVER_ERROR.getCode());
              try
              {
                  resultCode = this.getRemoteService().invoke(model);
              }
              catch (CommonException e)
              {
                  resultCode = ResultCode.error(e.getErrCode().getCode());
              }
              catch (Exception e)
              {
                  log.error("unknown error in channel.", e);
              }
              finally
              {
                  resultCode.setReqId(model.getReqId());
                  resultCode.setCost(System.currentTimeMillis() - model.getStart());
              }
              return resultCode;
          }
      
          /**
           * 获取远程调用的结果(根据业务情况去覆写,可以不需要remote服务调用)
           *
           * @param model 业务模型
           */
          protected ResultCode<List<O>> invokeBatchResult(T model)
          {
              return this.getRemoteService().invokeBatch(model);
          }
      
          /**
           * 注入远程服务
           *
           * @return 远程服务
           */
          protected RemoteService<O, T> getRemoteService()
          {
              return remoteService;
          }
      
          /**
           * 注入业务服务
           */
          @Autowired(required = false)
          private BaseBizService<T> service;
      
          /**
           * 注入远程调用服务
           */
          @Resource(name = BootConst.DEFAULT_REMOTE_SVC)
          private RemoteService<O, T> remoteService;
      
          /**
           * 配置的聚合服务
           */
          @Autowired
          private AssemblyConfService assemblyConfService;
      }
      
      • Service层抽象是整个业务逻辑抽象的核心,因为核心的业务流程都在且只应该在服务层;
    • RestService基类中,主要注入了业务配置缓存服务业务参数缓存服务BaseBizService<T> serviceAssemblyConfService assemblyConfService、和远程服务RemoteService<O,T>remoteService。逻辑就是通过入参T模型拿到真正的业务参数,并丰富上相关的阈值等配置,再来调用远程服务(此步骤可选),获取最终结果。
    • RestService基类中注入的带Guava缓存的BaseBizService抽象类代码如下:
      @Slf4j
      public abstract class BaseBizService<T extends BaseSecurity> implements Service<T>
      {
          @Override
          public List<T> getBatch(T model)
          {
              List<T> results = Lists.newArrayList();
              String key = StringUtils.EMPTY;
              try
              {
                  if (hasCached())
                  {
                      key = model.toBatchKey();
                      if (!StringUtils.isEmpty(key))
                      {
                          results = batchCache.get(key);
                      }
                  }
              }
              catch (ExecutionException e)
              {
                  log.error("no batch cache[{}] found:{},with exception:{}", this.getClass().getSimpleName(), key, e);
              }
              return results;
          }
      
          /**
           * 是否有缓存(可覆写)
           *
           * @return 默认有缓存
           */
          protected boolean hasCached()
          {
              return true;
          }
      
          /**
           * 批量的本地缓存对象
           */
          private final LoadingCache<String, List<T>> batchCache = CacheFactory.create(new CacheLoader<String, List<T>>()
          {
              @Override
              public List<T> load(String key)
              {
                  return queryBatchByKey(key);
              }
      
              @Override
              public Map<String, List<T>> loadAll(Iterable<? extends String> keys)
              {
                  List<T> queryResults = queryBatchByKeys(keys);
                  if (CollectionUtils.isEmpty(queryResults))
                  {
                      return Maps.newHashMap();
                  }
      
                  Map<String, List<T>> results = Maps.newHashMap();
                  for (T model : queryResults)
                  {
                      String key = model.toKey();
                      List<T> subResults = results.get(key);
                      if (null == subResults)
                      {
                          subResults = Lists.newArrayList();
                          results.put(key, subResults);
                      }
                      subResults.add(model);
                  }
                  return results;
              }
          });
      }
      
    • 业务服务中还有注入一个定时器任务RefreshCacheScheduleTask, 会根据Redis的标记改变而清除掉Guava缓存,代码如下:
      @Component("globalCacheRefreshTask")
      public class RefreshCacheScheduleTask implements ScheduleTask
      {
          @Override
          public void doTask(String key)
          {
              //1.获取最新的缓存value
              String value = IdUtil.uuid();
              Boolean nxResult = redis.setNx(key, value);
              if (Boolean.FALSE.equals(nxResult))
              {
                  value = redis.get(key);
              }
      
              //2.如果和上次的一致,则说明缓存已经刷新过了,否则需要刷新缓存
              if (!value.equalsIgnoreCase(lastId))
              {
                  LOGGER.info("Now clearing all cached data by key:{}", key);
                  CacheFactory.invalidateAll();
                  this.lastId = value;
              }
          }
      
          /**
           * redis服务
           */
          @Autowired
          private RedisService redis;
      
          /**
           * 执行id
           */
          private String lastId;
      }
      

      总结下通用的缓存刷新机制:

      • 通过BaseBizService和RefreshCacheScheduleTask配合,就完成了带业务缓存的查询和缓存刷新;
      • 仔细观察,其实是需要Redis的数据发生变化时,缓存才会失效,谁来触发Redis的数据变化呢?答案是通过后台管理bq-mgr)工具来完成,这样就做到了缓存刷新的异步化;
      • AssemblyConfService配置缓存服务,其实核心就是内部继承了BaseBizService做了配置参数的缓存,代码略;
      • 远程服务BaseRemoteService则是抽象了查询远程接口的参数,比如应该调用的远程接口url是什么,这个远程接口是否允许访问等,代码如下:
        public abstract class BaseRemoteService<O, T extends BaseBiz<O>> implements RemoteService<O, T>
        {
            @Override
            public ResultCode<List<O>> invokeBatch(T model)
            {
                ResultCode<List<O>> resultCode = null;
                try
                {
                    //1.获取接口的配置状态(是否可用)
                    boolean channelStatus = this.queryChannelStatus(model, GlobalDict.toChannelStatus().getKey());
                    if (!channelStatus)
                    {
                        LOGGER.error("[{}]channels[{}] is not active.", model.getUrlId(), model.getChannelId());
                        resultCode = ResultCode.error(ErrCodeEnum.CHANNEL_ERROR.getCode());
                        return resultCode;
                    }
        
                    //2.获取接口是否是驼峰结构的参数
                    boolean snake = this.queryChannelStatus(model, GlobalDict.toChannelSnake().getKey());
                    String resultJson = this.call(model, snake);
        
                    if (StringUtils.isEmpty(resultJson))
                    {
                        LOGGER.error("no channel[{}] results found.", model.getUrlId());
                        resultCode = ResultCode.error(ErrCodeEnum.SERVER_ERROR.getCode());
                        return resultCode;
                    }
        
                    resultCode = toModels(resultJson, model.toTypeRefs(), snake);
                    if (null == resultCode)
                    {
                        LOGGER.error("channel[{},{}]'s results have happened error.", model.getUrlId(), model.getUrlId());
                        resultCode = ResultCode.error(ErrCodeEnum.CHANNEL_ERROR.getCode());
                        return resultCode;
                    }
                }
                finally
                {
                    if (null != resultCode)
                    {
                        resultCode.setRespId(model.getRespId());
                        resultCode.setChannelId(model.getChannelId());
                    }
                }
                return resultCode;
            }
        
            /**
             * 真实的远程调用(可覆写)
             *
             * @param model 业务模型
             * @param snake 渠道是否驼峰转下划线方式
             * @return 结果json
             */
            protected String call(T model, boolean snake)
            {
                String channelUrl = this.getChannelUrl(model);
                if (StringUtils.isEmpty(channelUrl))
                {
                    LOGGER.error("[{}]no channel[{}] url found.", model.getUrlId(), model.getChannelId());
                    return null;
                }
        
                String paramJson = JsonUtil.toJson(model.toRemote(), snake);
                return this.restTemplate.invoke(channelUrl, null, paramJson);
            }
        
            /**
             * 查询渠道对应的url
             *
             * @param model 业务模型
             * @return 渠道的url
             */
            protected String getChannelUrl(T model)
            {
                Map<String, String> channelUrls = assemblyConfService.getChannelUrl();
                return channelUrls.get(model.getChannelId());
            }
        
            /**
             * 把结果转换成标准的带返回状态标记的业务模型(可覆写)
             *
             * @param json    返回结果json
             * @param typeRef 复杂类型的jackson转换适配器
             * @param snake   渠道是否驼峰转下划线方式
             * @return 带返回状态标记的业务模型
             */
            protected ResultCode<O> toModel(String json, TypeReference<ResultCode<O>> typeRef, boolean snake)
            {
                if (StringUtils.isEmpty(json))
                {
                    return null;
                }
                return JsonUtil.toComplex(json, typeRef, snake);
            }
        
            /**
             * 把结果转换成标准的带返回状态标记的业务模型(可覆写)
             *
             * @param json     返回结果json
             * @param typeRefs 复杂类型的jackson转换适配器
             * @param snake    渠道是否驼峰转下划线方式
             * @return 带返回状态标记的业务模型
             */
            protected ResultCode<List<O>> toModels(String json, TypeReference<ResultCode<List<O>>> typeRefs, boolean snake)
            {
                if (StringUtils.isEmpty(json))
                {
                    return null;
                }
                return JsonUtil.toComplex(json, typeRefs, snake);
            }
        
            /**
             * 查询渠道配置的特定状态
             *
             * @param model 业务模型
             * @param key   渠道状态参数
             * @return 渠道配置的布尔值
             */
            private boolean queryChannelStatus(T model, String key)
            {
                GlobalConfig config = new GlobalConfig();
                config.setUrlId(model.getChannelId());
                Map<String, String> dictMap = assemblyConfService.getChannelDict(config);
                String result = Boolean.FALSE.toString();
                if (dictMap.containsKey(key))
                {
                    result = dictMap.get(key);
                }
                return Boolean.TRUE.toString().equalsIgnoreCase(result);
            }
        
            /**
             * 日志句柄
             */
            private static final Logger LOGGER = LoggerFactory.getLogger(BaseRemoteService.class);
        
            /**
             * 配置的聚合服务
             */
            @Autowired(required = false)
            private AssemblyConfService assemblyConfService;
        
            /**
             * 注入http请求
             */
            @Autowired(required = false)
            private CommonRestTemplate restTemplate;
        }
        

        在远程调用时,其实还通过AOP做了渠道限流,参见熔断降级与限流在开源SpringBoot/SpringCloud微服务框架的最佳实践 文档;

  • 上面介绍了业务抽象的整个实现过程,总结如下:
    • 在场景类似的不同业务中,其实是可以通过泛型+抽象类来实现业务抽象的,抽象后,很多业务特性可以在这些公共的代码中去实现,这样大家都不用都写一遍了。其抽象实现包括:
      • 通过SpringMVC拦截器实现了所有业务的客户限流;
      • 通过Guava+Redis+定时器实现了所有业务的业务缓存+缓存刷新机制;
      • 通过SpringAOP切面实现了所有接口的渠道限流;
      • 通过日志注解+切面+部分框架代码可以轻松实现客户调用日志记录、渠道调用日志记录;
    • 泛型+抽象类通常难以理解,需要较为深厚的编码功底;

5.2.3 抽象业务的具体实现

  • 在上述框架代码抽象的基础上,我们来看看实现一个接口开发,需要做哪些事情:
    • 编写业务Rest QrCodeController,其代码如下:
      @Slf4j
      @RestController
      public class QrCodeController extends BaseBizController<QrCodeResult, QrCode, QrCodeInner>
      {
          /**
           * 获取Jwk公钥
           *
           * @param inner 业务入参模型
           * @return 公钥结果对象
           */
          @PostMapping("/demo/jwk")
          @Override
          public ResultCode<QrCodeResult> execute(@RequestBody QrCodeInner inner)
          {
              log.info("current inner:{}", JsonUtil.toJson(inner));
              return restService.execute(inner.toModel());
          }
      
          /**
           * 注入自定义的Rest服务
           */
          @Resource(name = DemoConst.DEMO_REST_SERVICE)
          private RestService<QrCodeResult, QrCode> restService;
      }
      
    • 编写业务服务QrRestServiceImpl,其代码如下:
      @Service(DemoConst.DEMO_REST_SERVICE)
      public class QrRestServiceImpl extends BaseRestService<QrCodeResult, QrCode>
      {
          @Override
          protected RemoteService<QrCodeResult, QrCode> getRemoteService()
          {
              return remoteService;
          }
      
          /**
           * 注入远程服务名
           */
          @Resource(name = DemoConst.DEMO_REMOTE_SERVICE)
          private RemoteService<QrCodeResult, QrCode> remoteService;
      }
      
    • 编写远程服务QrRemoteServiceImpl:
      @Slf4j
      @Service(DemoConst.DEMO_REMOTE_SERVICE)
      public class QrRemoteServiceImpl extends BaseRemoteService<QrCodeResult, QrCode>
      {
          @Override
          protected String call(QrCode model, boolean snake)
          {
              log.info("current param2:{},snake:{}", JsonUtil.toJson(model), snake);
              String channelUrl = this.getChannelUrl(model);
              if (StringUtils.isEmpty(channelUrl))
              {
                  log.error("[{}]no channel[{}] url found.", model.getUrlId(), model.getChannelId());
                  return null;
              }
      
              ResponseEntity<String> jwkJson = restTemplate.getForEntity(channelUrl, String.class);
              log.info("remote result:{}", jwkJson.getBody());
              return jwkJson.getBody();
          }
      
          @Override
          protected ResultCode<QrCodeResult> toModel(String json, TypeReference<ResultCode<QrCodeResult>> typeRef,
              boolean snake)
          {
              if (null == json)
              {
                  return ResultCode.error(ErrCodeEnum.SERVER_ERROR.getCode());
              }
              QrCodeResult result = new QrCodeResult();
              result.setOpenId(Hex.toHexString(json.getBytes(StandardCharsets.UTF_8)));
              return ResultCode.ok(result);
          }
      
          /**
           * 注入远程服务
           */
          @Autowired
          private CommonRestTemplate restTemplate;
      }
      
  • 在抽象业务框架的基础上做业务实现的总结:
    • 在框架实现了较多公共能力的基础上,业务逻辑实现非常简单;
    • 上述业务逻辑的实现逻辑不太容易理解,除了有良好的编码功底,还需要较好地团队沟通能力;

5.3 错误码兼容处理

  • 错误码兼容处理的思路:
    • 错误码的抽象同上述抽象业务逻辑的模型抽象类似,分为远程调用的错误码(errcode_inner)、标准错误码(errcode)和对外的兼容错误码(errcode_outer),分别在对应3个不同的国际化文件(选择国际化配置文件是为了以后可以支持错误码的语言切换);
    • inner_errcode在远程调用结束时,就先转化成标准错误码,然后返回至核心业务服务中;
    • 业务服务的标准接口则基于标准错误码做业务逻辑处理;
    • 在兼容接口的RestController中,还需要把标准错误码转换成兼容的错误码;
  • 错误码文件内容如下:
    • 内部错误码主要基于远程调用的三方服务错误码定义,并从中映射成标准错误码。配置文件路径为:errcode/errcode_inner_zh_CN.properties。内容如下:
      101.MSG=成功.
      101.OUT=100001
      999.MSG=失败.
      999.OUT=100099
      

      其中.MSG前面的是三方服务的错误码,=后面的是三方服务的错误码描述;
      其中.OUT前面的是三方服务的错误码,=后面的是我们系统定义的标准错误码;

    • 标准错误码是我们自己的业务定义。配置文件路径为:errcode/errcode_zh_CN.properties。内容如下:
      100001.MSG=通过
      100001.OUT=0
      100002.MSG=签名验证失败
      100002.OUT=-1
      100003.MSG=流量超限
      100003.OUT=-1
      100004.MSG=认证失败
      100004.OUT=-1
      100005.MSG=参数错误
      100005.OUT=-1
      100098.MSG=内部错误
      100098.OUT=-1
      100099.MSG=未通过
      100099.OUT=-1
      
      • 其中.OUT前面的是标准错误码,=后面的是兼容老接口的错误码;
      • 从中我们还可以看出,通过这种配置方式,可以适配出老接口的一个错误码对应多个错误Message的不合理但又必须要支持的诉求;
    • 兼容错误码是兼容老接口的错误码。配置文件路径为:errcode/errcode_outer_zh_CN.properties。内容如下:
      0=通过.
      -1=未通过.
      
  • 错误码加载实现ErrCodeMgr,代码如下:
    public final class ErrCodeMgr
    {
        /**
         * 获取标准的全局的内部错误码对象
         *
         * @return 标准的内部错误的错误码对象
         */
        public static ErrCode getServerErr()
        {
            String code = ErrCodeEnum.SERVER_ERROR.getCode();
            String msgKey = code + CODE_SUFFIX;
            String msg = I18N.get(Locale.SIMPLIFIED_CHINESE, msgKey);
            if (StringUtils.isEmpty(msg))
            {
                msg = StringUtils.EMPTY;
            }
            return ErrCode.build(code, msg);
        }
    
        /**
         * 获取外部错误码对象
         *
         * @param outCode 外部错误码code
         * @return 外部错误码对象
         */
        public static ErrCode getOut(String outCode)
        {
            return getOut(outCode, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 获取外部错误码对象
         *
         * @param outCode 外部错误码code
         * @param locale  语言
         * @return 外部错误码对象
         */
        public static ErrCode getOut(String outCode, Locale locale)
        {
            if (StringUtils.isEmpty(outCode))
            {
                LOGGER.error("invalid out code[{}/{}].", outCode, locale);
                return getToOut(ErrCodeEnum.SERVER_ERROR.getCode());
            }
            String outMsg = I18N.getOut(locale, outCode);
            if (StringUtils.isEmpty(outMsg))
            {
                LOGGER.error("not exist out code[{}/{}].", outCode, locale);
                return getToOut(ErrCodeEnum.SERVER_ERROR.getCode(), locale);
            }
            return ErrCode.build(outCode, outMsg, OUT_TYPE);
        }
    
        /**
         * 获取标准的错误码(默认中文)
         *
         * @param code 错误码code
         * @return 错误码对象
         */
        public static ErrCode get(String code)
        {
            return get(code, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 获取参数校验标准的错误码(默认中文)
         *
         * @param code 错误码code
         * @return 错误码对象
         */
        public static ErrCode getValid(String code)
        {
            return getValid(code, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 获取标准的错误码
         *
         * @param code   错误码code
         * @param locale 语言
         * @return 错误码对象
         */
        public static ErrCode get(String code, Locale locale)
        {
            if (StringUtils.isEmpty(code))
            {
                LOGGER.error("no standard code[{}/{}].", code, locale);
                return getServerErr();
            }
            String msgKey = code + CODE_SUFFIX;
            String msg = I18N.get(locale, msgKey);
            if (StringUtils.isEmpty(msg))
            {
                LOGGER.error("not exist standard code[{}/{}].", code, locale);
                return getServerErr();
            }
            return ErrCode.build(code, msg);
        }
    
        /**
         * 获取标准的错误码(带detail,适用于参数校验场景)
         *
         * @param code   错误码code
         * @param locale 语言
         * @return 错误码对象
         */
        public static ErrCode getValid(String code, Locale locale)
        {
            if (StringUtils.isEmpty(code))
            {
                LOGGER.error("no standard parameter code[{}/{}].", code, locale);
                return getServerErr();
            }
            String[] codes = StringUtils.split(code, Const.LINK);
            String realCode = codes[0];
            ErrCode errCode = get(realCode);
            if (null == errCode)
            {
                LOGGER.error("no standard code[{}/{}] in parameter config.", code, locale);
                return getServerErr();
            }
            //只有当参数校验对应的标准错误码存在时,才添加detail
            if (code.contains(errCode.getCode()))
            {
                errCode.setDetail(I18N.getValid(code));
            }
            return errCode;
        }
    
        /**
         * 根据内部错误码获取标准错误码(默认获取中文)
         *
         * @param inCode 内部错误码
         * @return 标准错误码对象
         */
        public static ErrCode getFromIn(String inCode)
        {
            return getFromIn(inCode, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 根据内部错误码获取标准错误码
         *
         * @param inCode 内部错误码code
         * @param locale 语言
         * @return 标准错误码对象
         */
        public static ErrCode getFromIn(String inCode, Locale locale)
        {
            String key = inCode + OUT_SUFFIX;
            String code = I18N.getIn(locale, key);
            if (StringUtils.isEmpty(code))
            {
                LOGGER.error("no in code[{}/{}] to standard.", inCode, locale);
                return getServerErr();
            }
    
            return get(code, locale);
        }
    
        /**
         * 根据标准错误码获取外部错误码(默认获取中文)
         *
         * @param code 标准错误码
         * @return 外部错误码对象
         */
        public static ErrCode getToOut(String code)
        {
            return getToOut(code, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 根据标准错误码获取外部错误码
         *
         * @param code   标准错误码
         * @param locale 语言
         * @return 外部错误码对象
         */
        public static ErrCode getToOut(String code, Locale locale)
        {
            String outKey = code + OUT_SUFFIX;
            String outCode = I18N.get(locale, outKey);
            if (StringUtils.isEmpty(code) || StringUtils.isEmpty(outCode))
            {
                LOGGER.error("no code[{}/{}] to out.", code, locale);
                return getToOut(ErrCodeEnum.SERVER_ERROR.getCode());
            }
    
            return getOut(outCode, locale);
        }
    
        /**
         * 加载标准的错误码的国际化文件中的内容(默认添加中文和英文)
         *
         * @param path 文件路径(注意不能带语言及properties后缀,即不能包含zh_CN.properties)
         */
        public static void load(String path)
        {
            I18N.loadI18n(path, Locale.US);
            I18N.loadI18n(path, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 加载外部错误码的国际化文件中的内容(默认添加中文和英文)
         *
         * @param path 文件路径(注意不能带语言及properties后缀,即不能包含zh_CN.properties)
         */
        public static void loadOut(String path)
        {
            I18N.loadI18nOut(path, Locale.US);
            I18N.loadI18nOut(path, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 加载内部错误码的国际化文件中的内容(默认添加中文和英文)
         *
         * @param path 文件路径(注意不能带语言及properties后缀,即不能包含zh_CN.properties)
         */
        public static void loadIn(String path)
        {
            I18N.loadI18nIn(path, Locale.US);
            I18N.loadI18nIn(path, Locale.SIMPLIFIED_CHINESE);
        }
    
        /**
         * 加载参数校验错误码的国际化文件中的内容(默认添加中文和英文)
         *
         * @param path 文件路径(注意不能带语言及properties后缀,即不能包含zh_CN.properties)
         */
        public static void loadValid(String path)
        {
            I18N.loadI18nValid(path, Locale.US);
            I18N.loadI18nValid(path, Locale.SIMPLIFIED_CHINESE);
        }
    
        private ErrCodeMgr()
        {
        }
    
        /**
         * 日志句柄
         */
        private static final Logger LOGGER = LoggerFactory.getLogger(ErrCodeMgr.class);
    
        /**
         * 引入错误码的国际化管理器
         */
        private static final ErrCodeI18nMgr I18N = ErrCodeI18nMgr.getInstance();
    
        /**
         * 错误码国际化文件中的code key后缀
         */
        private static final String CODE_SUFFIX = ".MSG";
    
        /**
         * 错误码国际化文件中的code映射到外部的code的key后缀
         */
        private static final String OUT_SUFFIX = ".OUT";
    
        /**
         * 外部错误码的类型
         */
        private static final int OUT_TYPE = 1;
    }
    
    • 上述三种错误码是独立加载的,但是其API都是类似的,目前默认支持的是中文。

5.4 接口兼容适配

  • 处理思路:
    • 重新定义一套和原老接口入参和出参完全一样的接口RestController,在其调用服务层之前,先把入参模型转换成标准RestController的入参模型;
    • 这样兼容接口的RestController就可以复用标准的服务层逻辑;
    • 服务层处理完毕后,在兼容接口的RestController中根据拿到的标准出参模型,再转换成老接口的出参模型;
  • 通过接口举例来说明:
    • 标准接口的入参body json中只有一个code 字符串字段时,其返回值的body json的data字段为一个模型对象,入参出参分别如下所示:
      curl --location 'http://localhost:9992/demo/jwk' \
      --header 'Authorization: Bearer eyJ...' \
      --header 'Content-Type: application/json' \
      --data '{
          "code":"test123"
      }'
      

      {
          "req_id": "661e1cc825c94074a750c3b3ef351259",
          "resp_id": "d8fb21b7b52248caa256192116fb0e13",
          "code": "100001",
          "msg": "通过",
          "data": {
              "open_id": "7b226b657973223a..."
          },
          "cost": 201
      }
      
    • 其兼容的老接口的body json中有一个code 字符串数组字段,其返回值的body json的data字段为一个模型对象集合。
  • 对应的编码实现:
    • 定义一个兼容的入参模型OldQrCodeInner,并覆写其code属性为字符串集合,代码如下:
      @Data
      public class OldQrCodeInner extends BaseBizInner<QrCode>
      {
          @Override
          protected QrCode genModel()
          {
              String realCode = StringUtils.EMPTY;
              if (!CollectionUtils.isEmpty(code))
              {
                  realCode = code.get(0);
              }
              QrCode qrCode = new QrCode();
              qrCode.setCode(realCode);
              return qrCode;
          }
      
          /**
           * 扫码时的code
           */
          private List<String> code;
      }
      
    • 定义一个兼容的老接口OldQrCodeController,并覆写其错误码和入参转换,最终获得了完全兼容的结果。其代码如下:
      @Slf4j
      @RestController
      public class OldQrCodeController extends BaseBizController<QrCodeOuter, QrCode, QrCodeInner>
      {
          /**
           * 获取Jwk公钥
           *
           * @param inner 业务入参模型
           * @return 公钥结果对象
           */
          @PostMapping("/demo/jwk/old")
          public ResultCode<List<QrCodeOuter>> execute(@RequestBody OldQrCodeInner inner)
          {
              log.info("current inner:{}", JsonUtil.toJson(inner));
              //1.通过老接口的入参获取标准的业务模型
              QrCode qrCode = inner.toModel();
              //2.通过标准入参调用标准的服务
              ResultCode<QrCodeOuter> resultCode = restService.execute(qrCode);
              String code = resultCode.getCode();
              List<QrCodeOuter> outers = null;
              if (null != resultCode.getData())
              {
                  outers = Lists.newArrayList(resultCode.getData());
              }
              //3.根据标准服务的标准错误码,获取兼容错误码
              ErrCode oldErrCode = ErrCodeMgr.getToOut(code);
              //4.拼接兼容错误码、兼容出参的结果对象
              return ResultCode.build(oldErrCode, outers);
          }
      
          /**
           * 注入自定义的Rest服务
           */
          @Resource(name = DemoConst.DEMO_REST_SERVICE)
          private RestService<QrCodeOuter, QrCode> restService;
      }
      
  • 验证效果如下:
    • 兼容老接口的入参为:
      curl --location 'http://localhost:9992/demo/jwk/old' \
      --header 'Authorization: Bearer eyJ...' \
      --header 'Content-Type: application/json' \
      --data '{
          "code":["test123"]
      }'
      
    • 兼容老接口的出参(返回结果)为:
      {
          "code": "0",
          "msg": "通过.",
          "data": [
              {
                  "open_id": "7b226b657973223a5b7b22..."
              }
          ],
          "cost": 0
      }
      
    • 调用报错时,需要参考bq-bizREADME.MD文档配置兼容接口/demo/jwk/old访问权限,步骤如下:
      -- 1.录入请求的url(如:/demo/jwk/old)
      INSERT INTO public.bq_global_dict(id, key, value, type)
      VALUES ('d221', 'DEMO_OLD_QR_API', '/demo/jwk/old', 'ClientUrl');
      
      -- 2.录入请求的url对应的配置参数(如:/demo/jwk/old)
      INSERT INTO public.bq_global_dict(id, key, value, type)
      VALUES ('d223', 'DEMO_OLD_QR_API', 'client.to.channel', 'ChannelConfig');
      
      -- 3.录入请求的url对应的渠道参数(如:/demo/jwk/old)
      INSERT INTO public.bq_global_config(id, svc_id, client_id, url_id, svc_value, create_time)
      VALUES ('svc311', 'client.to.channel', 'app001', 'DEMO_OLD_QR_API', 'DEMO_CHANNEL_JWK_API', '1566382443412');
      
      -- 4.录入请求的url对应的渠道URL参数(如:/demo/jwk/old),存在时可以跳过
      INSERT INTO public.bq_global_dict(id, key, value, type)
      VALUES ('d414', 'DEMO_CHANNEL_JWK_API', 'http://bq-auth/oauth/jwk', 'ChannelUrl');
      
      -- 5.录入请求的url对应的渠道结果处理参数(如:/demo/jwk/old),存在时可以跳过
      INSERT INTO public.bq_global_dict(id, key, value, type)
      VALUES ('d420', 'channel.status', 'true', 'DEMO_CHANNEL_JWK_API');
      INSERT INTO public.bq_global_dict(id, key, value, type)
      VALUES ('d421', 'channel.snake', 'true', 'DEMO_CHANNEL_JWK_API');
      
      -- 6.配置客户调用限流(如:/demo/jwk/old)
      INSERT INTO public.bq_global_config(id, svc_id, client_id, url_id, svc_value, create_time)
      VALUES ('svc441', 'client.limit.qps', 'app001', 'DEMO_OLD_QR_API', '100', '1566382443412');
      INSERT INTO public.bq_global_config(id, svc_id, client_id, url_id, svc_value, create_time)
      VALUES ('svc442', 'client.limit.max', 'app001', 'DEMO_OLD_QR_API', '100000', '1566382443412');
      
      -- 7.配置客户调用对应的渠道限流(如:/demo/jwk/old)
      INSERT INTO public.bq_global_config(id, svc_id, client_id, url_id, svc_value, create_time)
      VALUES ('svc444', 'channel.limit.qps', 'DEMO_OLD_QR_API', 'DEMO_CHANNEL_JWK_API', '110', '1566382443412');
      INSERT INTO public.bq_global_config(id, svc_id, client_id, url_id, svc_value, create_time)
      VALUES ('svc445', 'channel.limit.max', 'DEMO_OLD_QR_API', 'DEMO_CHANNEL_JWK_API', '150000', '1566382443412');
      

6. 思考总结

  • 服务的兼容无感升级是非常常见的现实诉求,因为大多情况下,都是先有了系统,并且在持续迭代中,架构严重腐化,无以为继,不得不升;
  • 大多公司的业务都具有相似性,可以采用泛型+业务抽象的方式,把大部分通用能力、公共业务流程给固化下来,可以提升编码质量,同时减少业务开发工作量;
  • 上述沉淀的代码架构图中,其组件部分基本上是和业务无关的,有较强的通用性;其微服务部分的架构设计,重点考虑了业务安全和高性能,如:鉴权和认证分离、数据读写分离、缓存及刷新机制等,还考虑了系统后续的升级改造,尽量不把自己绑死在某个中间件上;
  • 本人只是列举了无感迁移的部分典型问题,实际迁移过程比这复杂很多倍,限于精力有限,无法一一列举;

7. 参考资料

  • [1] AWS S3签署和对 REST 请求进行身份验证

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/736382.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BFF网关模式开发指南

BFF是近些年新衍生出来的一种开发模式&#xff0c;或者说是一种适配模式的系统&#xff0c;BFF全称为Backend OF Front意为后端的前端&#xff0c;为了适配微服务模式下前端后端系统接口调用混乱而出现的。在如今微服务盛行的趋势下&#xff0c;大型系统中划分出了数十个服务模…

前端优化的一些方向

对于浏览器来说&#xff0c;加载网页的过程可以分为两部分&#xff0c;下载文档并响应&#xff08;5%左右&#xff09;&#xff0c;下载各种组件&#xff08;95%左右&#xff09;。 而对比大部分优秀网页来说下载文档&#xff08;10%~ 20%&#xff09;&#xff0c;下载组件&…

23_7第一周LeetCode刷题回顾

目录 1. 两数之和2. 两数相加3.无重复字符的最长子串4.寻找两个正序数组的中位数5.最长回文子串6.N 形变换7.整数反转8.字符串转整数&#xff08;atoi&#xff09;9.回文数10. 正则表达式匹配11. 盛最多水的容器12. 整数转罗马数字13. 罗马数字转整数14. 最长公共前缀15.三数之…

MyBatis中的动态SQL(sql标签、where标签、set标签、批量增加与批量删除)

目录 sql标签 ​编辑 where标签 set标签 foreach标签 批量增加 批量删除 将基础SQL语句中重复性高的增加它的复用性&#xff0c;使得sql语句的灵活性更强 sql标签<sql> <sql id"text">select * from user</sql><select id"selectA…

如何在苹果商店发布App?

一、介绍 众所周知&#xff0c;苹果对于自家产品的安全问题十分重视&#xff0c;他们有严格的一套审核标准和流程&#xff0c;当我们想要在苹果商店发布一款App的时候就需要经过重重艰难险阻&#xff0c;克服不少繁杂的问题去完成这项工作。 另外有一点需要注意的是&#xff…

C语言库函数strcpy学习

strcpy是C语言的一个标准库函数&#xff1b; strcpy把含有\0结束符的字符串复制到另一个地址空间&#xff0c;返回值的类型为char*。 原型声明&#xff1a;char *strcpy(char* dest, const char *src); 头文件&#xff1a;#include <string.h> 和 #include <stdio.h&g…

领域驱动设计(三) - 快速开始 - 【3/3】事件风暴

使用DDD的最终目的是深入学习业务如何运作。然后基于学习试验、质疑、再学习和重建模的过程。过程中面临的最大挑战是如何快速学习&#xff0c;并且在保证学习质量的前提下压缩学习时间&#xff08;你的学习是需要公司付工资的&#xff09;。 事件风暴就是一种相对高效的分析工…

【电子学会】2023年05月图形化二级 -- 接水果

接水果 天上掉落各种水果下来&#xff0c;有草莓、苹果、香蕉&#xff0c;快拿大碗去接住水果吧。 1. 准备工作 &#xff08;1&#xff09;导入背景Blue Sky&#xff1b; &#xff08;2&#xff09;删除小猫角色&#xff0c;导入角色Bowl、Apple、Strawberry、Bananas。 2.…

【技能实训】DMS数据挖掘项目-Day03

文章目录 任务5【任务5.1】基础信息实体类【任务5.2.1】继承DataBase类&#xff0c;重构日志类【任务5.2.2】继承DataBase类&#xff0c;重构物流实体类【任务5.2.3】创建物流、日志测试类&#xff0c;测试任务5.2中的程序&#xff0c;演示物流信息、日志信息的采集及打印输出 …

【Redis】Transaction(事务)

&#x1f3af;前言 Redis事务是一个组有多个Redis命令的集合&#xff0c;这些命令可以作为一个原子操作来执行。 Redis事务通常用于以下两种情况&#xff1a; 保证操作的原子性&#xff1a;在多个命令的执行过程中&#xff0c;如果有一个命令执行失败&#xff0c;整个事务都需…

【数据编制架构】数据编织(Data fabric)架构完整指南

本文探讨了 Data Fabric 的内容、原因、方式和人员&#xff0c;包括 Data Fabric 架构、挑战、优势、核心功能、供应商等。 Data Fabric——以数据为中心的企业的“必备” 在过去几年中&#xff0c;“Data Fabric”一词已成为企业数据集成和管理的代名词。分析公司 Gartner 将“…

vtkButtonWidget Window 添加按钮

有时我们需要在 VTK 窗口中增加 按钮&#xff0c;右上角&#xff1b; 实现&#xff0c;通过回调函数&#xff0c;vtkButtonCallback 获取点击&#xff1a; #include <vtkVersion.h> #include <vtkSmartPointer.h>#include <vtkPolyDataMapper.h> #include &…

Java性能权威指南-总结27

Java性能权威指南-总结27 数据库性能的最佳实践Java集合类API同步还是非同步设定集合的大小 集合与内存使用效率 数据库性能的最佳实践 Java集合类API Java的集合类API有很大的选择余地&#xff1b;Java 7至少提供了58个不同的集合类。在编写应用时&#xff0c;选择恰当的集合…

数据结构算法题——数组

leetcode-1.两数之和 leetcode-1.两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在…

阿里云AliYun物联网平台使用-申请免费试用及完成初始配置

一、项目简介 本专栏文章将围绕阿里云物联网平台&#xff0c;实现其设备向云平台的数据上传&#xff0c;客户端获取云平台数据。设备通过NBIOT技术实现无线采集&#xff0c;定时上传。 二、阿里云平台申请 阿里云物联网平台试用申请地址 进入上述超链接网址&#xff1a; 由于是…

【InnoDB 存储引擎】15.7.1 InnoDB Locking(锁实验,包含了如 记录锁、间隙锁、Next-Key Lock 算法等,重要)

文章目录 1 关于 Record Lock 的实验1.1 实验 1&#xff1a;没有主键时的如何锁定1.2 实验 1&#xff08;续&#xff09;&#xff1a;带着问题继续实验1.3 实验 2&#xff1a;有主键时如何锁定 2 关于 Next-Key Lock 的实验2.1 实验 3&#xff1a;如何确定算法的锁定范围2.2 实…

VS 字体不对齐解决方案

1. 问题描述 输入相同数量但不是同一类型的字符的字符&#xff0c;会出现字符显示不对齐的问题。 在某些需要根据对齐来写的代码的时候&#xff0c;这种情况是相当的折磨。 2. 解决方案 设置等宽字体。 依次点击 VS 上方的 工具 → 选项 → 字体和颜色 → 字体 → 随便选择一款…

基于simulink识别彩色视频序列中的交通警告标志

一、前言 此示例演示如何识别彩色视频序列中的交通警告标志&#xff0c;如“停止”、“请勿进入”和“让行”。 二、模型 下图显示了交通警告标志识别模型&#xff1a; 三、交通警告标志模板 该示例使用两组模板 - 一组用于检测&#xff0c;另一组用于识别。 为了节省计算…

Linux常用命令——ex命令

在线Linux命令查询工具 ex 启动vim编辑器的ex编辑模式 补充说明 在ex模式下启动vim文本编辑器。ex执行效果如同vi -E&#xff0c;适用于法及参数可参照vi指令&#xff0c;如要从Ex模式回到普通模式&#xff0c;则在vim中输入:vi或:visual即可。 语法 ex&#xff08;参数&…

【算法设计与分析】集合相等问题——设计一个拉斯维加斯算法,对于给定的集合S和T,判定其是否相等。

目录 一、问题描述二、问题分析三、运行结果四、源代码 一、问题描述 给定两个集合S和T&#xff0c;试设计一个判定S和T是否相等的蒙特卡洛算法。 数据输入&#xff1a; 由文件input.txt给出输入数据。第1行有1个正整数n&#xff0c;表示集合的大小。接下来的2行&#xff0c;每…