距离上篇对账文章也有几个月之久,对账二期系统早已如期上线。 对于该系统,目前只有两个字,稳定得一比。
对账二期针对支付宝和微信千万级订单量对账时间在3分钟内完成对账&缓存存储(根据订单号查询平台方订单数据)。(公司业务上升很快,具体数字,涉及公司机密,不便泄漏)
由于对账一期在Redis上踩的坑,并且Redis内存需求会越来越大,成本高,对账二期未使用Redis。
使用RocksDB分布式数据库进行单机的本地存储(ESSD/SSD硬盘,ESSD性能为SSD百倍多,强烈推荐ESSD),极大的减少了成本,极大的增加了稳定性、准确性与性能。
关于系统架构与系统优化等等的一些坑在上篇文章已经介绍,在这里不会重复介绍一些类似的坑。
基于SpringBoot的对账系统实现的一个比较不错的架构如下:
对账单下载组件每天定时触发,从支付通道服务器上下载对账单。 在调度中心进行分配不同的对账系统进行不同的任务,可以按照通道划分任务,也可以按照业务系统订单维度划分任务。
对账系统处理完,进行入库或者缓存。选择差异处理方式,自动或者人工。 全部处理完成,进行通知到相应人员。
在数据层,数据量大,亦可以选择HBase等大数据存储数据库。
实际方案中,请采用简单阉割版架构(请看一期对账的系统)。
千万级别订单,每天使用磁盘空间大约为5G左右。建议硬盘使用云盘追加空间。(存储10天内订单数据即可,除非是想做成大数据,另说),建议是云盘,前期100GB即可(后期可扩展)。
一般来说,对账仅仅对前一日的订单数据,打款数据,所以,历史数据不需要存储太久,10天前的订单文件可随时删除。(如果实在需要一直存下去,增加云盘即可,每天半夜将10天前的订单文件移到另外的云盘)
如需查询历史订单数据,使用RocksDB按照订单维度进行存储订单。
序列化框架使用FST即可。不推荐别的。
另外,关于GC方面,推荐使用G1收集器,相对CMS收集器对账时间可以优化半分钟以上。
前面讲到了不使用Redis,而使用RocksDB来进行对账,那么如何进行。 RocksDB使用起来非常方便,在这里,我将依赖和工具类贴一下(RocksDB是我在学习区块链中学的,比特币区块链存储也是基于RocksDB)。
<!-- rocksdb -->
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>5.9.2</version>
</dependency>
<!-- fst -->
<dependency>
<groupId>de.ruedigermoeller</groupId>
<artifactId>fst</artifactId>
<version>2.52</version>
</dependency>
由于RocksDB都是操作字节,所以需要序列化工具类,在这里推荐FST。
import org.nustaq.serialization.FSTConfiguration;
/**
* @author chenhx
* @version FstSerializerUtil.java, v 0.1 2018-10-19 下午 8:24
*/
public class FstSerializerUtil {
static FSTConfiguration configuration = FSTConfiguration.createDefaultConfiguration();
private FstSerializerUtil() {
}
@SuppressWarnings("unchecked")
public static <T> T deserialize(byte[] data) {
return (T) configuration.asObject(data);
}
public static <T> byte[] serialize(T obj) {
return configuration.asByteArray(obj);
}
}
使用非常的简单,下面看RocksDB工具类
/**
* 存储
*
* @author chenhx
* @version RocksDB.java, v 0.1 2018-10-13 下午 4:46
*/
@Slf4j
@Data
public class RocksDBUtils {
/**
* 对账数据文件
*/
public final static String DB_FILE = "rocksdb.file";
private volatile static RocksDBUtils instance;
private RocksDB db;
private RocksDBUtils() {
openDB();
}
public static RocksDBUtils getInstance() {
if (instance == null) {
synchronized (RocksDBUtils.class) {
if (instance == null) {
instance = new RocksDBUtils();
}
}
}
return instance;
}
/**
* 打开数据库
*/
private void openDB() {
try {
db = RocksDB.open(DB_FILE);
} catch (RocksDBException e) {
log.error("Fail to open db ! ", e);
throw new RuntimeException("Fail to open db ! ", e);
}
}
/**
* 删除数据
*/
private boolean delete(String key) {
try {
byte[] keyByte = FstSerializerUtil.serialize(key);
db.delete(keyByte);
return true;
} catch (Exception e) {
log.error("删除{}数据异常", key, e);
}
return false;
}
private byte[] getBytes(String key) throws RocksDBException {
byte[] keyByte = FstSerializerUtil.serialize(key);
return db.get(keyByte);
}
/**
* 设置数据
*/
private boolean set(String key, byte[] stringSet) throws RocksDBException {
byte[] keyByte = FstSerializerUtil.serialize(key);
db.put(keyByte, stringSet);
return true;
}
/**
* 设置对账单数据
*/
public <T> boolean set(String key, T stringSet) throws RocksDBException {
return set(key, FstSerializerUtil.serialize(stringSet));
}
public <T> T get(String key) throws RocksDBException {
byte[] bytes = getBytes(key);
if (bytes != null) {
return FstSerializerUtil.deserialize(bytes);
}
return null;
}
/**
* 关闭数据库
*/
public void closeDB() {
try {
if (db != null) {
db.close();
}
} catch (Exception e) {
log.error("Fail to close db ! ", e);
}
}
}
然后对于RocksDB的操作就非常简单了。
RocksDB是无法追加数据和修改数据的。 因为在订单加载是分批加载到内存,而且由于要节省内存,是无法一次性将订单全部加载完的。 即使是使用了取模,还是无可避免的会遇到订单需要追加到RocksDB的情况。 在这里,我使用的解决办法是。不使用单个key的追加,而使用多个有规律的key进行追加数据,这样即使在多线程中,也不会产生并发影响,并且实现了数据量的追加存储。
取订单数据也非常方便,模和数据追加的key是固定存在某个key下的。 画个图理解:
另外还遇到这样一个情况,在开发中(emmmm,幸好没上线,不然就是事故了),遇到表被迁库的情况,而且不是一个服务器下了。没有通知到我。其他人也不知道我用到了
我这边使用到了其中一个被迁的表,并且是连表的操作,而且基本不可能进行不连表操作,除非是砍需求。问题就这么来了。为什么不能拆分进行,因为这两张表数据太多了,两张表都是千万上亿的数据量,我这里不可能进行拆分SQL的,为什么,因为另外一张表我只用到了一个字段,但是没办法,只有那个表才有那个字段。 库是不可能再迁移回来了,不要想,迁走就是为了减少库的容量,而且改了很多业务。
在这里我使用A表和B表表示吧,B表是被迁移的表,A在databaseA,B在databaseB。我这里使用到了B表中的一个字段b。
然后和DBA,架构师等等讨论了很多方案,其中一个可行方案是,使用阿里云的数据订阅,而且要将A表和B表都进行订阅到databaseC。这样,我可以继续我的连表操作。但是,开支高啊,就为了一个非常简单的需求,要订阅两次,emmm,小姐姐提的需求,怎么的也得完成。
最终还是没有采用该方案。 因为,过了半天以后,终于在A表中发现了一个废弃字段,而该字段正好可以存放我需要的B表中的字段b,只需要通知到新增B表数据,修改B表数据中该字段的开发人员,将对应业务进行修改即可,美滋滋。
该问题肯定是可以避免的,但是执行起来还是有种种问题,最大问题就是信息同步,如果涉及到大的改动,现在,只能是通知到每个人(基本不可能)。如果在迁库的之前就知道了,那么进行迁库方案的人肯定会想另外的解决办法,这次是正好有一个废弃字段,下次就不一定了。
但是如何知道某个人某个项目使用了哪个数据,最好的方法就是,读库的项目只需要一个,另外需要数据的项目,全部从该项目的接口中获取。将公司项目进行服务化,避免出现你也随便读库,我也随便读库的情况发生。只有越规范,问题才会越少。
信息同步一直以来都是大公司中普遍存在的问题,人多以后,难免有沟通成本,难免有信息丢失。 对于信息不同步的情况,大家有什么好的建议和处理方式,都可以在评论中进行留言,大家共同探讨。
没关注公众号的朋友,可以关注一波,干货多多