从Oracle到国产数据库:一卡通系统的千万级并发实践

0 阅读12分钟

兼容 是对前人努力的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点

说到一卡通,咱们北京的同志们肯定都不陌生。不管是坐公交地铁,还是去便利店买点东西,刷一下就完事儿了。但就是这么个看似简单的动作,背后可是有套非常复杂的系统在支撑。今天就跟大家聊聊,咱们是怎么把这套核心系统从国外数据库迁移到国产数据库上的,中间踩了不少坑,也积累了不少经验。

为什么要折腾这个事儿

其实一开始我们也没想着要换数据库,原来的Oracle跑得挺稳的。但是国家有政策要求嘛,信创这个事儿是必须得做的。而且说实在的,长期依赖国外产品,心里总是不踏实,万一哪天人家不给你用了,或者涨价了,那不就傻眼了嘛。

清结算系统是整个一卡通的核心,每天要处理上千万笔交易,涉及到资金流转、对账这些关键业务。一旦出了问题,那可是大事。所以这次改造必须得慎重,不能随便换个数据库就完事儿了。

原来的系统架构是这样的:

Oracle 12c RAC集群
├── 主节点(IBM Power8)
│   └── 数据量: 10TB+
│   └── 表数量: 1000+张
│   └── 单表数据: 部分超亿行
└── 备用节点(ADG容灾)

新系统采用全栈国产方案:

国产数据库集群(金仓数据库KES)
├── 主节点(海光CPU + 银河麒麟OS)
│   └── 读写分离架构
├── 备节点1(同机房)
└── 备节点2(异机房容灾)

遇到的几个大坑

第一个坑:数据量太大,迁移时间不够用

一开始我们预估的迁移时间是一个晚上,但实际测试发现根本不够。10TB的数据量,就算网络再快,也得搬好久啊。而且关键是,我们只有2小时的割接窗口,这期间还不能影响业务正常运行。

怎么办呢?后来想了个办法,分阶段来:

-- 第一阶段:全量数据迁移(提前几天做)
SELECT * FROM original_oracle_tables 
INTO new_kingbase_tables;

-- 第二阶段:增量数据同步(割接前启动)
-- 使用KFS工具实时同步Oracle和金仓之间的增量数据
-- 这样割接的时候只需要同步最后几分钟的数据

-- 第三阶段:数据一致性校验
-- 比对两边的数据,确保没丢数据

实际操作下来,全量迁移花了差不多3天时间,但这都是在不影响业务的情况下提前完成的。割接的时候只花了不到2小时,主要是同步最后那点增量数据。

第二个坑:并发量太大了,性能扛不住

早高峰那会儿,3个小时内要处理千万级的事务,每秒钟可能就有上万笔交易进来。这个压力确实不小。

一开始测试的时候,新系统的性能还不如老系统,这可把我们急坏了。后来跟数据库厂商的技术人员一起分析,发现主要是这么几个问题:

-- 问题1:索引设计不合理
-- 原来的索引在金仓上效果不好,需要重新设计

-- 问题2:事务处理方式需要调整
-- Oracle和金仓的事务处理机制不一样,有些SQL需要改写

-- 问题3:参数配置没优化
-- 内存分配、并发参数这些都需要根据实际负载调整

举个例子,我们有个商户额度更新的场景,在高并发情况下性能特别差。原来的SQL是这样的:

-- 原来的写法
UPDATE merchant_account 
SET balance = balance - :amount 
WHERE merchant_id = :merchant_id;

这个在高并发情况下锁竞争很严重,后来改成了这样:

-- 优化后的写法
SELECT balance FROM merchant_account 
WHERE merchant_id = :merchant_id 
FOR UPDATE;

-- 在应用层面计算新余额
-- 然后执行更新
UPDATE merchant_account 
SET balance = :new_balance 
WHERE merchant_id = :merchant_id 
AND balance = :old_balance;

这样改动之后,单行更新性能提升了差不多30%,效果还是很明显的。

第三个坑:微服务拆分后,各种资源冲突

原来的系统是单体架构,后来拆成了一百多个微服务,这下问题就来了。特别是晚上跑批处理的时候,经常跟数据抽取任务撞车,导致系统崩溃。

// 问题场景:晚上跑批和数据抽取同时进行
// 结果:CPU、IO资源争抢严重,系统卡死

我们的解决办法是:

-- 1. 给不同的微服务分配资源配额
ALTER RESOURCE POOL batch_service 
WITH (CPU_SHARE=30, MEMORY_SHARE=20);

ALTER RESOURCE POOL data_extraction 
WITH (CPU_SHARE=20, MEMORY_SHARE=10);

-- 2. 优化vacuum参数,减少清理对业务的影响
SET maintenance_work_mem = '2GB';
SET autovacuum_vacuum_scale_factor = 0.1;

-- 3. 把只读查询分流到只读节点
-- 交易写操作走主库,报表查询走备库

还有一些查询SQL特别大,生成临时文件的时候把磁盘空间撑爆了。这种SQL我们都是逐个优化,要么拆分成小查询,要么加上limit限制。

第四个坑:JDBC连接超时

这个问题挺搞的,应用端老是报JDBC连接超时的错。一开始以为是网络问题,后来发现是数据库的心跳机制设置太激进了。

# 原来的配置
jdbc.connection.timeout=5000
jdbc.socket.timeout=10000

# 调整后的配置
jdbc.connection.timeout=30000
jdbc.socket.timeout=60000
# 增加重试机制
jdbc.retry.on.failure=true
jdbc.retry.max.attempts=3

调整之后,这个问题就基本解决了。其实很多时候网络抖动是难免的,关键是要有容错机制。

技术方案是怎么落地的

双轨并行方案

为了降低风险,我们采用了双轨并行的上线方案。简单说就是新老系统同时运行一段时间,数据实时同步,新系统逐步接管业务流量。

-- 数据同步架构
Oracle(源库) 
  ↓ KFS实时同步
KINGBASE(新库)
  ↓ 复制
KINGBASE只读节点

-- 应用层面
原应用 → Oracle
新应用 → KINGBASE

这样做的好处是,万一新系统出问题了,可以立马切回老系统,风险完全可控。实际运行了一个月左右,确认新系统没问题了,才完全切过去。

容灾方案设计

一卡通这种系统,可用性要求极高。我们设计了同城容灾方案,两个机房各有一套完整的数据库集群。

机房A: 主集群(一主两备)
├── 主节点(读写)
├── 备节点1(只读,查询服务)
└── 备节点2(只读,分析服务)

机房B: 容灾集群
└── 备节点(异地容灾)
-- 查看复制状态
SELECT * FROM sys_stat_replication;

-- 手动切换主备(紧急情况)
SELECT pg_switchover();

-- 验证数据同步延迟
SELECT now() - replay_timestamp_lag FROM pg_stat_replication;

实际测试过,故障情况下秒级就能切换过去,业务基本无感知。

性能监控与调优

上线之后我们建立了完善的监控体系,实时跟踪系统运行状态。

-- 慢查询监控
SELECT query, mean_exec_time, calls 
FROM pg_stat_statements 
WHERE mean_exec_time > 1000
ORDER BY mean_exec_time DESC 
LIMIT 10;

-- 锁等待监控
SELECT pid, usename, query, wait_event_type, wait_event 
FROM pg_stat_activity 
WHERE wait_event IS NOT NULL;

-- 表膨胀监控
SELECT schemaname, tablename, 
       pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
       n_dead_tup, n_live_tup 
FROM pg_stat_user_tables 
WHERE n_dead_tup > n_live_tup;

遇到性能问题的时候,一般是先查这几个视图,基本上能定位到大部分问题。

数据模型设计的那些事儿

一卡通系统的数据模型设计,说实话挺有讲究的。核心就是几个原则:

流水表设计

交易流水是整个系统的核心,必须保证数据完整性和可追溯性。

CREATE TABLE transaction_ledger (
    txn_id VARCHAR(32) NOT NULL,
    card_no VARCHAR(32) NOT NULL,
    merchant_id VARCHAR(32) NOT NULL,
    amount BIGINT NOT NULL,
    txn_time TIMESTAMP NOT NULL,
    txn_type VARCHAR(16) NOT NULL,
    status VARCHAR(16) NOT NULL,
    trace_no VARCHAR(64) NOT NULL,
    create_time TIMESTAMP NOT NULL,
    PRIMARY KEY (txn_id)
) PARTITION BY RANGE (txn_time);

-- 按月分区,方便历史数据归档
CREATE TABLE transaction_ledger_202401 PARTITION OF transaction_ledger
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE transaction_ledger_202402 PARTITION OF transaction_ledger
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- 关键索引
CREATE INDEX idx_ledger_card_time ON transaction_ledger(card_no, txn_time);
CREATE INDEX idx_ledger_merchant_time ON transaction_ledger(merchant_id, txn_time);
CREATE INDEX idx_ledger_trace ON transaction_ledger(trace_no);

分区的好处是显而易见的,查询的时候可以根据时间范围直接定位到对应分区,性能提升很明显。

幂等性控制

金融系统最怕重复记账,必须要有幂等性控制机制。

-- 幂等控制表
CREATE TABLE transaction_idempotent (
    idem_key VARCHAR(64) NOT NULL,
    txn_id VARCHAR(32) NOT NULL,
    create_time TIMESTAMP NOT NULL,
    PRIMARY KEY (idem_key)
);

-- 事务处理伪代码
BEGIN;
-- 1. 先插入幂等表
INSERT INTO transaction_idempotent (idem_key, txn_id, create_time)
VALUES (:trace_no, :txn_id, now());

-- 2. 插入流水
INSERT INTO transaction_ledger (txn_id, card_no, merchant_id, amount, txn_time, txn_type, status, trace_no, create_time)
VALUES (:txn_id, :card_no, :merchant_id, :amount, now(), :txn_type, 'SUCCESS', :trace_no, now());

-- 3. 更新账户余额
UPDATE account_balance 
SET balance = balance - :amount 
WHERE account_id = :account_id AND balance >= :amount;

COMMIT;

这样设计的好处是,就算网络超时重试,也不会重复记账,因为幂等表的主键约束会阻止重复插入。

余额表设计

余额表的设计要特别小心,避免并发问题。

CREATE TABLE account_balance (
    account_id VARCHAR(32) NOT NULL,
    balance BIGINT NOT NULL,
    frozen_amount BIGINT NOT NULL DEFAULT 0,
    version BIGINT NOT NULL DEFAULT 1,
    update_time TIMESTAMP NOT NULL,
    PRIMARY KEY (account_id)
);

-- 乐观锁更新
UPDATE account_balance 
SET balance = balance - :amount,
    version = version + 1,
    update_time = now()
WHERE account_id = :account_id 
  AND version = :current_version;

-- 检查影响的行数,如果为0说明已经被别人修改过了,需要重试

使用乐观锁可以避免长事务持有锁,提高并发性能。

运维经验总结

系统上线运行快三年了,中间也遇到过各种问题,总结了一些经验教训。

备份恢复策略

备份这种事情,平时可能觉得没用,但真要出问题的时候,那就是救命稻草。

# 每天全量备份
pg_dump -Fc -f /backup/full_$(date +%Y%m%d).dmp mydb

# 每小时增量备份(基于WAL)
pg_receivewal -D /backup/wal

# 定期测试恢复
pg_restore -d testdb /backup/full_20240101.dmp

我们每个月都会做一次恢复演练,确保备份文件真的能用。

权限管理

权限这块一定要管严了,特别是生产环境。

-- 创建应用账号,只给必要的权限
CREATE USER app_user WITH PASSWORD 'xxx';
GRANT SELECT, INSERT, UPDATE ON transaction_ledger TO app_user;
GRANT SELECT, UPDATE ON account_balance TO app_user;

-- 管理账号单独管理
CREATE USER admin_user WITH PASSWORD 'xxx';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin_user;

-- 敏感操作记录审计
ALTER SYSTEM SET log_statement = 'mod';

参数调优

数据库参数调优是个持续的过程,需要根据实际负载情况不断调整。

-- 内存参数
SET shared_buffers = '32GB';
SET work_mem = '64MB';
SET maintenance_work_mem = '2GB';

-- 并发参数
SET max_connections = 500;
SET max_worker_processes = 16;
SET max_parallel_workers_per_gather = 4;

-- WAL参数
SET wal_buffers = '16MB';
SET checkpoint_completion_target = 0.9;
SET max_wal_size = '4GB';

这些参数都需要根据实际硬件配置和负载情况来调整,不能照抄照搬。

效果怎么样

经过这么一番折腾,效果还是不错的:

  • 性能提升: 单行更新性能提升30%,批量结算效率提升15%
  • 稳定性提升: 系统稳定运行近三年,交易成功率99.99%以上
  • 成本降低: 运维成本降低不少,而且不用再担心国外产品的授权费用
  • 自主可控: 核心技术完全掌握在自己手里,心里踏实多了

特别是早高峰那会儿,千万级的并发也能扛得住,晚上跑批处理也能顺利完成,没出过什么大问题。

遇到的一些奇葩问题

说点有趣的,上线过程中遇到过一些挺有意思的问题。

有一次系统突然变慢了,查了半天发现是一个开发人员写了个SQL查询全表扫描,而且还是在一个超大的表上。

-- 这个SQL直接把系统搞挂了
SELECT * FROM transaction_ledger 
WHERE card_no LIKE '%123%';

-- 后来改成了这样
SELECT * FROM transaction_ledger 
WHERE card_no = :card_no;

还有一次,事务号暴涨导致备机回卷,主库直接停止处理业务了。这个问题比较隐蔽,后来通过调整vacuum参数才解决。

-- 调整vacuum相关参数
SET autovacuum = on;
SET autovacuum_vacuum_threshold = 1000;
SET autovacuum_analyze_threshold = 500;
SET autovacuum_vacuum_scale_factor = 0.1;
SET autovacuum_analyze_scale_factor = 0.05;

后续的优化计划

虽然系统已经稳定运行了,但还是有优化的空间:

  • 分布式架构: 考虑引入分布式数据库,进一步提升并发处理能力
  • 缓存优化: 热点数据加入缓存层,减少数据库压力
  • 智能运维: 引入AI技术,实现故障预测和自动调优
  • 微服务拆分: 进一步拆分微服务,实现更精细的资源管控

总结一下

这次数据库迁移项目确实挺有挑战性的,但结果还是值得的。关键是要有科学的方法论:

  1. 充分评估: 不要盲目开始,先做充分的评估和测试
  2. 分步实施: 不要想着一次性搞定,分阶段进行更稳妥
  3. 双轨运行: 新老系统并行一段时间,降低风险
  4. 持续监控: 上线后要密切关注系统运行状态
  5. 快速响应: 遇到问题要及时响应,不能拖

国产数据库经过这么多年的发展,其实已经相当成熟了,完全可以胜任核心业务系统的要求。关键是要根据实际业务场景来做合理的架构设计和调优,不能照搬照抄。

这次项目也证明了一个道理:只要方法得当,国产数据库完全可以在关键领域替代国外产品,而且还能做得更好。

希望这些经验能对大家有所帮助,如果有啥问题,欢迎交流讨论。

官网链接: kingbase.com.cn