概述
距离上篇对账文章也有几个月之久,对账二期系统早已如期上线。
对于该系统,目前只有两个字,稳定得一比。
对账二期针对支付宝和微信千万级订单量对账时间在 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 使用
引入 Maven 依赖
<!-- 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 工具类
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 是无法追加数据和修改数据的。
因为在订单加载是分批加载到内存,而且由于要节省内存,是无法一次性将订单全部加载完的。
即使是使用了取模,还是无可避免的会遇到订单需要追加到 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 表数据中该字段的开发人员,将对应业务进行修改即可,美滋滋。
该问题肯定是可以避免的,但是执行起来还是有种种问题,最大问题就是信息同步,如果涉及到大的改动,现在,只能是通知到每个人(基本不可能)。如果在迁库的之前就知道了,那么进行迁库方案的人肯定会想另外的解决办法,这次是正好有一个废弃字段,下次就不一定了。
解决方案
但是如何知道某个人某个项目使用了哪个数据,最好的方法就是,读库的项目只需要一个,另外需要数据的项目,全部从该项目的接口中获取。将公司项目进行服务化,避免出现你也随便读库,我也随便读库的情况发生。只有越规范,问题才会越少。
信息同步一直以来都是大公司中普遍存在的问题,人多以后,难免有沟通成本,难免有信息丢失。
对于信息不同步的情况,大家有什么好的建议和处理方式,都可以在评论中进行留言,大家共同探讨。
评论