`
eyesmore
  • 浏览: 376630 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

分布式交易对账日期的“跨年虫”问题

阅读更多

0、常识

 

分布式系统数据不一致性问题是不可避免,于是对于重要交易(跟钱打交道的,操作不具有“幂等性”的)的都需要对账,对账就有个对账日期(对账日期必须是对账各方共享的,一致认可的),这个日期不能采用自然时间,因为各个子系统的本地自然时间总存在一定的差异,再者交易还有时延,跨国交易还有时区问题。因此,对账日期希望由某个子系统分配,然后在交易请求或响应时发给其他子系统,实现共享。于是,分布式应用中,一笔交易往往需要: 发送方自然时间; 接受方自然时间; 财务时间(对账时间)。

 

1、具体场景
第三方支付机构(比如UMPAY,支付宝)要给银联发送一个扣款请求,银联顺利扣掉用户银行卡中的钱,给第三方支付机构发送“成功”的响应,并在响应中给出了该笔交易的对账日期。
要知道银联的报文格式都是ISO8583的,一般在第15域给出个格式为MMdd的对账日期。(注意:不是yyyyMMdd,省了个年份,跨年的时候就容易出错)。
而第三方支付机构由于是后来发展的,意识到类似千年虫问题的存在,于是在数据库设计时对账日期是以yyyyMMdd存放的。
于是接下来一个很自然的工作是:当银联给出MMdd的对账日期,第三方支付机构应该弄出个yyyyMMdd的对账日期。

 


2、款年时可能出现的问题
前提:银联和第三方支付机构的结算是按日结算
(1)财务日期超前自然日期(提前日切)
    【边界情况描述】
    当前自然时间2008-12-31 23:00:00,但是银联的财务日期提前切到2009-01-01;第三方支付机构给银联发一个扣款请求,并收到成功响应,财务日期给的是“0101”(MMdd格式);
    【第三方支付机构补齐年份】
    第三方支付机构依据本地系统时间2008-12-31 23:00:00,对“0101”补齐年份,得到SettlementDate=20080101,而当前自然日期是NaturalDate=20081231;
    由于双方协商的结算周期是“按日”,那么正常情况下SettlementDate与NaturalDate最多相差一天,现在却出现:Distance = |SettlementDate - NaturalDate| >> SettlementPeriod,于是判断本次计算出现了因跨年导致的年份不对的情况。if(SettlementDate < NaturalDate) SettlementDate.year += 1;最终得到的对账日期是:20090101
   
(2)财务日期滞后自然日期(滞后日切)
    【边界情况描述】
    当前自然时间2009-01-01 00:10:00,但是银联的财务日期尚未做日切,财务日期是2008-12-31;第三方支付机构给银联发一个扣款请求,并收到成功响应,财务日期给的是“1231”(MMdd格式);
    【第三方支付机构补齐年份】
    第三方支付机构依据本地系统时间2009-01-01 00:10:00,对“1231”补齐年份,得到SettlementDate=20091231,而当前自然日期是NaturalDate=20090101;
    由于双方协商的结算周期是“按日”,那么正常情况下SettlementDate与NaturalDate最多相差一天,现在却出现:Distance = |SettlementDate - NaturalDate| >> SettlementPeriod,于是判断本次计算出现了因跨年导致的年份不对的情况。if(SettlementDate > NaturalDate) SettlementDate.year -= 1;最终得到的对账日期是:20081231

(3)第三方支付机构得到“补齐年份”的算法
    String-yyyyMMdd SettlementDateNormalization(String MMdd-BankUnion) {
        String SettlementDate = System.currentTime("yyyy") + MMdd-BankUnion;
        String NaturalDate = System.currentTime("yyyyMMdd");
        long Distance = |SettlementDate - NaturalDate|;
        long SettlementPeriod = 1天;//双方协议约定的对账周期常量
        if(Distance >> SettlementPeriod) {//当时间差远远大于对账周期,则说明出现跨年的问题了
            if(SettlementDate < NaturalDate)  SettlementDateNormalized = SettlementDate.yyyy + 1;
            else SettlementDateNormalized = SettlementDate.yyyy - 1;
        }

        return SettlementDateNormalized;
}

(4)分析(3)中的算法
虽然双方协议约定的对账周期常量是1天,但是银联日切工作可能是人工干预的,这样可能因为认为因素导致好几天都没做日切,比如:当前自然时间已经是20090710号了,但是银联的财务日期却还是20090707号。按照上面的算法,第三方支付机构在补齐年份时,输入财务日期:“0707”,初步补齐是SettlementDate=“20090707”,本地自然日期是NaturalDate=“20090710”,两者之差Distance > SettlementPeriod;同时SettlementDate < NaturalDate,于是被规格化成“20100707”。
应该注意到跨年导致的Distance会远远大于SettlementPeriod,这个Distance都快接近一年了。随意算法:if(Distance >> SettlementPeriod)的“远远大于”的具体实施应该是:
if(Distance > SettlementPeriod * Tolerance=7) 最多容忍7天不做日切,或提前7天做日切。

String-yyyyMMdd SettlementDateNormalization(String MMdd-BankUnion) {
        String SettlementDate = System.currentTime("yyyy") + MMdd-BankUnion;
        String NaturalDate = System.currentTime("yyyyMMdd");
        long Distance = |SettlementDate - NaturalDate|;
        long SettlementPeriod = 1天;//双方协议约定的对账周期常量
        int Tolerance = 7;//对账日期切换相对协议滞后或提前的容忍度

        if(Distance > SettlementPeriod * Tolerance) {//当时间差远远大于对账周期,则说明出现跨年的问题了
            if(SettlementDate < NaturalDate)  SettlementDateNormalized = SettlementDate.yyyy + 1;
            else SettlementDateNormalized = SettlementDate.yyyy - 1;
        }
        return SettlementDateNormalized;
}

 

(5)java代码

/**
	 * @param	bankCheckDate	银行传入的清算日期,不带年的
	 * @return	本地补充一个带年的清算日期
	 * */
	protected String caculateBankCheckDate(String bankCheckDate) throws ParseException {
		String yyyyMMdd = null;
		String yyyy = Util.strDateTime("yyyy");
        String stldate = yyyy + bankCheckDate;
        SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");
        long slttime = fmt.parse(stldate).getTime();
        long currtime = System.currentTimeMillis();
        long dis = slttime - currtime;
        long day1 = 24 * 3600 * 1000;
        int tolerance = 7;
        if (Math.abs(dis) >= day1 * tolerance) {
            if (slttime < currtime) {
                yyyy = String.valueOf(Integer.parseInt(yyyy) + 1);
                stldate = yyyy + bankCheckDate;
            } else {
                yyyy = String.valueOf(Integer.parseInt(yyyy) - 1);
                stldate = yyyy + bankCheckDate;
            }
        }
        yyyyMMdd = stldate;
		return yyyyMMdd;
	}
 

(注:有时间需要整理下表达)

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics