注:上面并不是分布式事务 。在数据访问权限收口之前,它只存在于同一个JVM中 。如果项目允许,可以考虑使用Atomikos和Mybatis整合的方案 。
数据安全性
本次进行了很多代码改造,如何保证数据安全,保证数据不丢失,我们的机制如下,分为三种情况进行讨论:
- 跨库事务:6处,采用了代码保证一致性的改造方式;上线前经过重点测试,保证逻辑无问题;
- 单库事务:依赖于自定义事务实现,针对自定义事务实现这一个类进行充分测试即可,测试范围小,安全性有保障;
- 其余单表操作:相关修改是在mapper上添加了数据源切换注解,改动位置几百处,几乎是无脑改动,但也存在遗漏或错改的可能;测试同学可以覆盖到核心业务流程,但边缘业务可能会遗漏;我们添加了线上监测机制,当出现找不到表的错误时(说明数据源切换注解添加错误),记录当前执行sql并报警,我们进行逻辑修复与数据处理 。
综上,通过对三种情况的处理来保证数据的安全性 。
应用拆分
系统接近单体架构,存在以下风险:
- 系统性风险:一个组件缺陷会导致整个进程崩溃,如内存泄漏、死锁 。
- 复杂性高:系统代码繁多,每次修改代码都心惊胆战,任何一个bug都可能导致整个系统崩溃,不敢优化代码导致代码可读性也越来越差 。
- 测试环境冲突,测试效率低:业务都耦合在一个系统,只要有需求就会出现环境抢占,需要额外拉分支合并代码 。
与数据库拆分相同,系统拆分也是根据业务划分拆成9个新系统 。
方案一:搭建空的新系统,然后将老系统的相关代码挪到新系统 。
- 优点:一步到位 。
- 缺点:需要主观挑选代码,然后挪到新系统,可视为做了全量业务逻辑的变动,需要全量测试,风险高,周期长 。
方案二:从老系统原样复制出9个新系统,然后直接上线,通过流量路由将老系统流量转发到新系统,后续再对新系统的冗余代码做删减 。
- 优点:拆分速度快,首次上线前无业务逻辑改动,风险低;后续删减代码时依据接口调用量情况来判定,也可视为无业务逻辑的改动,风险较低,并且各系统可各自进行,无需整体排期,较为灵活 。
- 缺点:分为了两步,拆分上线和删减代码
文章插图
拆分方案对比
我们在考虑拆分风险和拆分效率后,最终选择了方案二 。
文章插图
方案二原理
拆分实践
- 搭建新系统
直接复制老系统代码,修改系统名称,部署即可
- 流量路由
路由器是拆分的核心,负责分发流量到新系统,同时需要支持识别测试流量,让测试同学可以提前在线上测试新系统 。我们这边用filter来作为路由器的,源码见下方 。
@Override public void doFilter(Servletrequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest servletRequest = (HttpServletRequest) request; HttpServletResponse servletResponse = (HttpServletResponse) response; // 路由开关(0-不路由, 1-根据指定请求头路由, 2-全量路由) final int systemRouteSwitch = configUtils.getInteger("system_route_switch", 1); if (systemRouteSwitch == 0) { filterChain.doFilter(request, response); return; } // 只路由测试流量 if (systemRouteSwitch == 1) { // 检查请求头是否包含测试流量标识 包含才进行路由 String systemRoute = ((HttpServletRequest) request).getHeader("systemRoute"); if (systemRoute == null || !systemRoute.equals("1")) { filterChain.doFilter(request, response); return; } } String systemRouteMapJsonStr = configUtils.getString("route.map", ""); Map map = JSONObject.parseobject(systemRouteMapJsonStr, Map.class); String rootUrl = map.get(servletRequest.getRequestURI()); if (StringUtils.isEmpty(rootUrl)) { log.error("路由失败,本地服务内部处理 。原因:请求地址映射不到对应系统, uri : {}", servletRequest.getRequestURI()); filterChain.doFilter(request, response); return; } String targetURL = rootUrl + servletRequest.getRequestURI(); if (servletRequest.getQueryString() != null) { targetURL = targetURL + "?" + servletRequest.getQueryString(); } RequestEntity requestEntity = null; try { log.info("路由开始 targetURL = {}", targetURL); requestEntity = createRequestEntity(servletRequest, targetURL); ResponseEntity responseEntity = restTemplate.exchange(requestEntity, byte[].class); if (requestEntity != null && requestEntity.getBody() != null && requestEntity.getBody().length > 0) { log.info("路由完成-请求信息: requestEntity = {}, body = {}", requestEntity.toString(), new String(requestEntity.getBody())); } else { log.info("路由完成-请求信息: requestEntity = {}", requestEntity != null ? requestEntity.toString() : targetURL); } HttpHeaders headers = responseEntity.getHeaders(); String resp = null; if (responseEntity.getBody() != null && headers != null && headers.get("Content-Encoding") != null && headers.get("Content-Encoding").contains("gzip")) { byte[] bytes = new byte[30 * 1024]; int len = new GZIPInputStream(new ByteArrayInputStream((byte[]) responseEntity.getBody())).read(bytes, 0, bytes.length); resp = new String(bytes, 0, len); } log.info("路由完成-响应信息: targetURL = {}, headers = {}, resp = {}", targetURL, JSON.toJSONString(headers), resp); if (headers != null && headers.containsKey("Location") && CollectionUtils.isNotEmpty(headers.get("Location"))) { log.info("路由完成-需要重定向到 {}", headers.get("Location").get(0)); ((HttpServletResponse) response).sendRedirect(headers.get("Location").get(0)); } addResponseHeaders(servletRequest, servletResponse, responseEntity); writeResponse(servletResponse, responseEntity); } catch (Exception e) { if (requestEntity != null && requestEntity.getBody() != null && requestEntity.getBody().length > 0) { log.error("路由异常-请求信息: requestEntity = {}, body = {}", requestEntity.toString(), new String(requestEntity.getBody()), e); } else { log.error("路由异常-请求信息: requestEntity = {}", requestEntity != null ? requestEntity.toString() : targetURL, e); } response.setCharacterEncoding("UTF-8"); ((HttpServletResponse) response).addHeader("Content-Type", "application/json"); response.getWriter().write(JSON.toJSONString(ApiResponse.failed("9999", "网络繁忙哦~,请您稍后重试"))); } }
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 玻璃饭盒可以在微波炉里面用吗
- 泡沫饭盒能放微波炉加热吗
- 玉米轻微发霉还能吃吗
- 康熙微服私访记铃铛记是第几部 康熙微服私访记铃铛记
- 怎么取消手机银行短信服务 怎么取消手机银行
- 服务器怎么用u盘装系统?云服务器是什么,要怎么使用?
- 冰箱上面可以放微波炉吗
- 泡泡堂服务器连接中断请稍后再试 泡泡堂服务器连接中断
- 微波炉上面可以放东西吗
- 完美微笑公式口诀图解?完美微笑公式的相关因素?
