发表时间:2009-07-11
最后修改:2009-07-11
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;
}
(注:有时间需要整理下表达)