说到一卡通,咱们北京的同志们肯定都不陌生。不管是坐公交地铁,还是去便利店买点东西,刷一下就完事儿了。但就是这么个看似简单的动作,背后可是有套非常复杂的系统在支撑。今天就跟大家聊聊,咱们是怎么把这套核心系统从国外数据库迁移到国产数据库上的,中间踩了不少坑,也积累了不少经验。
为什么要折腾这个事儿
其实一开始我们也没想着要换数据库,原来的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技术,实现故障预测和自动调优
- 微服务拆分: 进一步拆分微服务,实现更精细的资源管控
总结一下
这次数据库迁移项目确实挺有挑战性的,但结果还是值得的。关键是要有科学的方法论:
- 充分评估: 不要盲目开始,先做充分的评估和测试
- 分步实施: 不要想着一次性搞定,分阶段进行更稳妥
- 双轨运行: 新老系统并行一段时间,降低风险
- 持续监控: 上线后要密切关注系统运行状态
- 快速响应: 遇到问题要及时响应,不能拖
国产数据库经过这么多年的发展,其实已经相当成熟了,完全可以胜任核心业务系统的要求。关键是要根据实际业务场景来做合理的架构设计和调优,不能照搬照抄。
这次项目也证明了一个道理:只要方法得当,国产数据库完全可以在关键领域替代国外产品,而且还能做得更好。
希望这些经验能对大家有所帮助,如果有啥问题,欢迎交流讨论。
官网链接: kingbase.com.cn