如何完成日千万级别以上的订单对账(二)

https://blog.csdn.net/qq_26525215/article/details/85008363

概述
距离上篇对账文章也有几个月之久,对账二期系统早已如期上线。
对于该系统,目前只有两个字,稳定得一比。

对账二期针对支付宝和微信千万级订单量对账时间在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>
1
2
3
4
5
6
7
8
9
10
11
12
由于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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用非常的简单,下面看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);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
然后对于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表数据中该字段的开发人员,将对应业务进行修改即可,美滋滋。

该问题肯定是可以避免的,但是执行起来还是有种种问题,最大问题就是信息同步,如果涉及到大的改动,现在,只能是通知到每个人(基本不可能)。如果在迁库的之前就知道了,那么进行迁库方案的人肯定会想另外的解决办法,这次是正好有一个废弃字段,下次就不一定了。

解决方案
但是如何知道某个人某个项目使用了哪个数据,最好的方法就是,读库的项目只需要一个,另外需要数据的项目,全部从该项目的接口中获取。将公司项目进行服务化,避免出现你也随便读库,我也随便读库的情况发生。只有越规范,问题才会越少。

信息同步一直以来都是大公司中普遍存在的问题,人多以后,难免有沟通成本,难免有信息丢失。
对于信息不同步的情况,大家有什么好的建议和处理方式,都可以在评论中进行留言,大家共同探讨。

没关注公众号的朋友,可以关注一波,干货多多

--------------------- 
作者:谙忆 
来源:CSDN 
原文:https://blog.csdn.net/qq_26525215/article/details/85008363 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/xiyang_1990/article/details/85244884