一、前言
在系统开发过程中,有些业务功能面临日切
(日期切换)问题,比如结息跑批问题,在当前工作日临近24点的时候触发结息,实际交易时间我们预期的是当前时间,但是由于业务执行耗时,可能进行了日切,业务跑到下一个工作日了,这样业务如果采用下一个工作日的时间进行业务计算,可能会导致业务结果与预期不一致
,有没有什么解决方案呢?
二、解决方案
在这里,我们可以采用一种解决方案,就是交易时间不采用系统时间,交易时间的获取从上游应用(可以从时间服务器获取)请求传递下来,下游应用在处理业务时,采用传递的交易时间,而不是直接使用系统时间来处理。
下面采用一个案例进行演示说明:比如我们有一个利息结算场景,需要对金额进行每天产生的利息进行结算,某些请求可能在临近24点的时候触发,此时,下游系统处理时可能发生日切,此时我们要保证业务结果是符合预期的,按照上述描述进行演示。
1. 搭建nacos注册中心
我们这里采用两个应用来进行模拟业务请求,需要用到注册中心,这里采用Nacos作为注册中心:Nacos下载地址
Nacos部署可以参考:nacos安装手册
2. 创建服务提供方应用
首先创建一个简单spring Boot应用,当做服务提供方。
-
导入相关依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
增加相关配置
server:port: 7092 spring:cloud:nacos:discovery:namespace: publicserver-addr: 127.0.0.1:8848application:name: provider
-
编写业务逻辑代码:
import com.alibaba.nacos.api.utils.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;import java.math.BigDecimal; import java.time.LocalDate; import java.time.temporal.ChronoUnit; import java.util.Map;@RestController public class ProviderController {/*** 计算利息** @param paraMap* @return*/// 以下逻辑只是为了业务说明,并非真实业务@PostMapping("/calInterest")public BigDecimal calInterest(@RequestBody Map<String, Object> paraMap) {// 以下逻辑只是为了业务说明,并非真实业务// 获取交易日期String traceDateStr = (String) paraMap.get("traceDate");// 如果有交易日期则取当前日期,否则取系统时间LocalDate traceDate = StringUtils.isBlank(traceDateStr) ? LocalDate.now() : LocalDate.parse(traceDateStr);// 获取存款日期LocalDate savaDate = LocalDate.parse((String) paraMap.get("savaDate"));// 获取间隔天数long days = ChronoUnit.DAYS.between(savaDate, traceDate);// 获取利率BigDecimal rate = new BigDecimal((String) paraMap.get("rate"));// 获取金额BigDecimal money = new BigDecimal((String) paraMap.get("money"));return money.add(money.multiply(rate).multiply(BigDecimal.valueOf(days)));} }
注意:如果应用启动报com.alibaba.nacos.api.exception.NacosException: Client not connected, current status:STARTING
异常,则可能是Nacos服务版本和应用里面引入的nacos-client版本不匹配,需要进行匹配对应。
3. 创建发起方应用
-
导入发起方所需依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version> </dependency> <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId> </dependency>
-
增加相关配置
server:port: 7090 spring:cloud:nacos:discovery:namespace: publicserver-addr: 127.0.0.1:8848application:name: consumerconsumer:business:interval: 36000 # 业务处理与下一个工作日切换允许的时间间隔,超过这个时间,就需要传递交易时间,避免因业务在日切之前还未完成# 可以针对每个URL请求单独配置bm:- url: /calInterestinterval: 18000 feign:client:config:provider: # 只针对provider这个应用生效,也可以配置全局生效request-interceptors:- com.learn.interceptor.CustomFeignInterceptor
-
声明远程feign调用
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;import java.math.BigDecimal;
import java.util.Map;@FeignClient(name = "provider")
public interface CalInterestService {@PostMapping("/calInterest")BigDecimal calInterest(@RequestBody Map<String, Object> paraMap);
}
-
增加自定义配置读取
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;import java.util.List;@Component @ConfigurationProperties(prefix = "consumer.business") @Data public class BusinessDataPro {private long interval;private List<BusinessUrl> bm;@Datapublic static class BusinessUrl{private String url;private long interval;}}
-
增加feign拦截器,增加时间判断逻辑
import com.alibaba.fastjson.JSON; import com.learn.pro.BusinessDataPro; import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils;import java.time.Duration; import java.time.LocalTime; import java.util.List; import java.util.Map;@Component public class CustomFeignInterceptor implements RequestInterceptor {@Autowiredprivate BusinessDataPro businessDataPro;@Overridepublic void apply(RequestTemplate template) {String url = template.url();List<BusinessDataPro.BusinessUrl> bm = businessDataPro.getBm();long interval = businessDataPro.getInterval();if (!CollectionUtils.isEmpty(bm)) {for (BusinessDataPro.BusinessUrl businessUrl : bm) {if (businessUrl.getUrl().equals(url)) {interval = businessUrl.getInterval();break;}}}byte[] body = template.body();Map<String, Object> paraMap = (Map<String, Object>) JSON.parse(body);LocalTime now = LocalTime.now(); // 获取当前时间LocalTime midnight = LocalTime.MIDNIGHT; // 午夜时间long time = Duration.between(midnight, now).toMillis(); // 获取时间差值if (time > interval) {// 如果日切时间大于配置的交易执行上限,就不需要传递交易时间,由下游应用自己获取本地时间paraMap.put("traceDate", null);}template.body(JSON.toJSONString(paraMap));}}
-
定义业务Controller逻辑
import com.learn.controller.feign.CalInterestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController;import java.math.BigDecimal; import java.time.LocalDate; import java.util.HashMap; import java.util.Map;@RestController public class DateChangeController {@Autowiredprivate CalInterestService calInterestService;/*** 只是用于模拟业务操作,并非真实业务** @param paraMap* @return*/// 这里为了方便使用Map类型,实际业务开发中不能这么使用@PostMapping("/dcTest")public Map<String, Object> calInterest(@RequestBody Map<String, Object> paraMap) {String account = (String) paraMap.get("account");// 判断一些账户信息 start// 模拟业务try {Thread.sleep(100L);} catch (InterruptedException e) {e.printStackTrace();}// 判断一些账户信息 endparaMap.put("traceDate", LocalDate.now());BigDecimal bigDecimal = calInterestService.calInterest(paraMap);Map<String, Object> resultMap = new HashMap<>();resultMap.put("code", 200);resultMap.put("allCount", bigDecimal);return resultMap;}}
-
4. 测试
-
通过nacos查看应用注册是否正确
-
请求测试
三、最后
本文只是提供一种业务日切处理的大概思路,实际开发过程中,请以业务逻辑为根本,完善日切面临的问题解决方案,避免无脑照搬导致的业务异常。