在智慧城市与数字校园的浪潮下,一卡通系统早已超越了简单的支付功能,成为集身份认证、门禁、消费、数据聚合于一体的核心数字枢纽。面对数千万用户与每秒数万笔交易的峰值压力,其底层数据库架构的设计直接决定了系统的生死存亡。本文将深入剖析一个大型交通一卡通系统的国产化改造实践,揭示其如何通过创新的数据库架构与容器化部署 策略,成功应对极致的高并发挑战。
一、架构演进:从集中式单体到分布式微服务 传统的一卡通系统多采用集中式架构,将所有鸡蛋放在一个篮子里。这种设计在业务初期简单有效,但随着用户量和交易量的爆炸式增长,单点故障、性能瓶颈和扩展性差等问题暴露无遗。现代高并发系统必须走向分布式。
接入层 :负责与海量终端设备(POS机、闸机等)通信,兼容多种协议,是流量入口的第一道关卡。业务逻辑层 :采用微服务架构,将用户、交易、结算等核心功能解耦为独立服务。这为后续的容器化部署 和基于Kubernetes (K8s) 的容器编排 奠定了坚实基础,实现了资源的弹性伸缩。数据存储层 :本文的核心,采用高性能数据库集群,通过读写分离、分库分表等技术承载海量数据。这一演进遵循四大核心原则:高可用性 (RTO<30秒)、强数据一致性 (金融级ACID)、弹性扩展 以及安全合规 。
二、数据库选型与集群架构设计 数据库是系统的心脏,选型至关重要。在高并发一卡通场景下,需要综合考量:百万级TPC-C性能、TB级数据存储能力、99.999%的高可用性,以及在信创背景下的自主可控 要求。国产数据库在历经锤炼后,已成为可靠选择。
基于此,我们设计了如下核心集群架构:
-- 集群节点配置示例
CREATE CLUSTER card_cluster WITH (
cluster_type = 'streaming_replication',
primary_node = 'node1:5432',
standby_nodes = ARRAY['node2:5432', 'node3:5432', 'node4:5432'],
sync_standby_names = 'node2,node3',
application_name = 'card_system'
);
-- 读写分离配置
CREATE PUBLICATION card_publication FOR ALL TABLES;
CREATE SUBSCRIPTION card_subscription
CONNECTION 'host=node2 port=5432 dbname=carddb user=replicator'
PUBLICATION card_publication;
该架构的精髓在于:
主备同步 :采用物理日志流复制(如WAL),确保主备数据强一致。智能负载均衡 :写操作走主节点,读操作由代理层(如HAProxy)动态分发至多个备节点,极大提升吞吐量。同城双活容灾 :主备节点跨机房部署,结合双网架构,将故障切换时间压缩至秒级,实现业务无缝衔接。
[AFFILIATE_SLOT_1]
三、核心数据模型与表结构设计 良好的表结构是高性能的基石。一卡通系统核心表需在范式化与查询效率间取得平衡,特别是面对数十亿条交易流水时。
-- 用户信息表
CREATE TABLE user_info (
user_id BIGINT PRIMARY KEY,
user_name VARCHAR(50) NOT NULL,
id_card VARCHAR(18) UNIQUE NOT NULL,
phone VARCHAR(11),
email VARCHAR(100),
user_type SMALLINT NOT NULL DEFAULT 1, -- 1:学生 2:教职工 3:其他
status SMALLINT NOT NULL DEFAULT 1, -- 1:正常 2:挂失 3:冻结 4:注销
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_phone (phone),
INDEX idx_id_card (id_card),
INDEX idx_status (status)
) PARTITION BY RANGE (user_id);
-- 卡片信息表
CREATE TABLE card_info (
card_id VARCHAR(20) PRIMARY KEY,
user_id BIGINT NOT NULL,
card_type SMALLINT NOT NULL DEFAULT 1, -- 1:实体卡 2:虚拟卡
card_status SMALLINT NOT NULL DEFAULT 1, -- 1:正常 2:挂失 3:补办中 4:注销
balance DECIMAL(12, 2) NOT NULL DEFAULT 0.00,
daily_limit DECIMAL(10, 2) DEFAULT 500.00,
single_limit DECIMAL(10, 2) DEFAULT 100.00,
issue_date DATE NOT NULL,
expire_date DATE,
last_used_time TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user_info(user_id),
INDEX idx_user_id (user_id),
INDEX idx_card_status (card_status),
INDEX idx_last_used (last_used_time)
);
-- 交易流水表(按时间分区)
CREATE TABLE transaction_log (
trans_id BIGSERIAL PRIMARY KEY,
card_id VARCHAR(20) NOT NULL,
trans_type SMALLINT NOT NULL, -- 1:消费 2:充值 3:转账 4:退款
trans_amount DECIMAL(12, 2) NOT NULL,
before_balance DECIMAL(12, 2) NOT NULL,
after_balance DECIMAL(12, 2) NOT NULL,
terminal_id VARCHAR(20),
merchant_id VARCHAR(20),
location_info JSONB,
trans_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
status SMALLINT NOT NULL DEFAULT 1, -- 1:成功 2:失败 3:处理中
remark VARCHAR(200),
FOREIGN KEY (card_id) REFERENCES card_info(card_id),
INDEX idx_card_time (card_id, trans_time),
INDEX idx_merchant_time (merchant_id, trans_time),
INDEX idx_trans_type (trans_type, trans_time)
) PARTITION BY RANGE (trans_time);
-- 商户信息表
CREATE TABLE merchant_info (
merchant_id VARCHAR(20) PRIMARY KEY,
merchant_name VARCHAR(100) NOT NULL,
merchant_type SMALLINT NOT NULL, -- 1:食堂 2:超市 3:图书馆 4:其他
contact_phone VARCHAR(11),
contact_person VARCHAR(50),
settlement_rate DECIMAL(5, 4) DEFAULT 0.0000, -- 结算费率
settlement_account VARCHAR(30),
status SMALLINT NOT NULL DEFAULT 1,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_merchant_type (merchant_type),
INDEX idx_status (status)
);
关键设计包括:为高频查询字段(如用户ID、卡号、交易时间)建立复合索引;对交易流水等巨量表按时间进行分区;将频繁读取但不常变更的数据(如商户信息)适当冗余,以减少关联查询。
四、性能优化全链路策略 应对高并发,需构建从数据库到应用层的立体化优化体系。
1. 数据库层深度优化 索引是数据库的“导航仪”,其设计直接决定查询速度。
-- 复合索引设计
CREATE INDEX idx_card_trans ON transaction_log(card_id, trans_time DESC);
CREATE INDEX idx_merchant_trans ON transaction_log(merchant_id, trans_time DESC);
-- 函数索引
CREATE INDEX idx_user_name_lower ON user_info(LOWER(user_name));
-- 分区索引
CREATE INDEX idx_trans_time_local ON transaction_log(trans_time) LOCAL;
同时,必须优化SQL语句,避免全表扫描、使用绑定变量防止硬解析、并利用执行计划分析工具持续调优。
-- 避免SELECT *,只查询需要的字段
SELECT user_id, user_name, phone FROM user_info WHERE status = 1;
-- 使用覆盖索引
SELECT card_id FROM transaction_log
WHERE trans_time >= '2024-01-01' AND trans_time < '2024-02-01';
-- 分页优化(避免深度分页)
SELECT * FROM transaction_log
WHERE trans_id > ?
ORDER BY trans_id ASC
LIMIT 20;
2. 应用层缓存与读写分离 数据库不是万能的,大部分热点读请求应由缓存承接。我们设计多级缓存:本地缓存(如Caffeine)应对极热点数据,分布式缓存(如Redis)存储用户会话及常用数据。
// 缓存配置类
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 不同业务设置不同的过期时间
Map cacheConfigs = new HashMap<>();
cacheConfigs.put("userInfo", config.entryTtl(Duration.ofMinutes(30)));
cacheConfigs.put("cardInfo", config.entryTtl(Duration.ofMinutes(10)));
cacheConfigs.put("transaction", config.entryTtl(Duration.ofMinutes(5)));
cacheConfigs.put("merchant", config.entryTtl(Duration.ofHours(1)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.build();
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
当单库达到性能极限,分库分表 是必经之路。通常按用户ID哈希或交易时间范围进行拆分。
// 基于用户ID的分片策略
@Component
public class UserShardingStrategy implements PreciseShardingAlgorithm {
@Override
public String doSharding(Collection availableTargetNames,
PreciseShardingValue shardingValue) {
Long userId = shardingValue.getValue();
// 基于用户ID取模分片,分为4个库
int shardIndex = Math.abs(userId.hashCode() % 4);
for (String each : availableTargetNames) {
if (each.endsWith("_" + shardIndex)) {
return each;
}
}
throw new IllegalArgumentException("未找到匹配的数据源");
}
}
// 基于时间范围的分片策略(用于交易流水表)
@Component
public class TimeRangeShardingStrategy implements RangeShardingAlgorithm {
@Override
public Collection doSharding(Collection availableTargetNames,
RangeShardingValue shardingValue) {
Range range = shardingValue.getValueRange();
Date lower = range.lowerEndpoint();
Date upper = range.upperEndpoint();
List result = new ArrayList<>();
Set shardNames = getShardNamesBetween(lower, upper);
for (String each : availableTargetNames) {
if (shardNames.contains(each)) {
result.add(each);
}
}
return result;
}
private Set getShardNamesBetween(Date start, Date end) {
Set shardNames = new HashSet<>();
Calendar calendar = Calendar.getInstance();
calendar.setTime(start);
while (!calendar.getTime().after(end)) {
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
String shardName = String.format("trans_log_%d_%02d", year, month);
shardNames.add(shardName);
calendar.add(Calendar.MONTH, 1);
}
return shardNames;
}
}
结合读写分离 配置,将读压力分散到多个副本。
# application.yml 数据源配置示例
spring:
datasource:
dynamic:
primary: master # 设置默认数据源
strict: true # 严格匹配数据源,未找到报错
datasource:
master:
url: jdbc:kingbase8://master-host:5432/carddb?currentSchema=public
username: ${MASTER_DB_USER}
password: ${MASTER_DB_PASSWORD}
driver-class-name: com.kingbase8.Driver
slave1:
url: jdbc:kingbase8://slave1-host:5432/carddb?currentSchema=public
username: ${SLAVE_DB_USER}
password: ${SLAVE_DB_PASSWORD}
driver-class-name: com.kingbase8.Driver
slave2:
url: jdbc:kingbase8://slave2-host:5432/carddb?currentSchema=public
username: ${SLAVE_DB_USER}
password: ${SLAVE_DB_PASSWORD}
driver-class-name: com.kingbase8.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
五、高可用与容灾:构建“打不垮”的系统 高可用不是一种功能,而是一种贯穿始终的设计理念。我们建立了多级故障转移机制:
数据库层 :主节点故障时,监控系统自动触发备节点升主。
-- 主备切换脚本
CREATE OR REPLACE FUNCTION trigger_failover()
RETURNS void AS $$
DECLARE
current_primary VARCHAR(100);
new_primary VARCHAR(100);
BEGIN
-- 检测当前主节点状态
SELECT node_name INTO current_primary
FROM pg_stat_replication
WHERE state = 'streaming'
LIMIT 1;
-- 如果主节点不可用,选择新的主节点
IF current_primary IS NULL THEN
-- 选择同步状态最好的备节点作为新主
SELECT node_name INTO new_primary
FROM pg_stat_wal_receiver
WHERE status = 'streaming'
ORDER BY last_msg_send_time DESC
LIMIT 1;
-- 执行主备切换
PERFORM pg_promote(new_primary);
-- 更新路由配置
UPDATE system_config
SET config_value = new_primary
WHERE config_key = 'primary_db_node';
-- 记录切换日志
INSERT INTO failover_log
(old_primary, new_primary, failover_time, reason)
VALUES (current_primary, new_primary, NOW(), 'primary node failure');
END IF;
END;
$$ LANGUAGE plpgsql;
-- 创建定时检测任务
CREATE EVENT TRIGGER monitor_db_health
ON SCHEDULE EVERY 30 SECOND
DO
BEGIN
-- 检查主节点健康状态
IF NOT check_primary_health() THEN
PERFORM trigger_failover();
END IF;
END;
应用层 :服务实例故障时,由Kubernetes 或服务网格自动摘除故障实例并重启新实例。
// 数据库连接健康检查
@Component
@Slf4j
public class DatabaseHealthChecker {
@Autowired
private DataSource dataSource;
@Value("${health.check.interval:5000}")
private long checkInterval;
@PostConstruct
public void init() {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::checkDatabaseHealth,
0, checkInterval, TimeUnit.MILLISECONDS);
}
private void checkDatabaseHealth() {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// 执行简单查询检查连接
ResultSet rs = stmt.executeQuery("SELECT 1");
if (rs.next()) {
HealthMonitor.setDatabaseStatus(HealthStatus.UP);
}
} catch (SQLException e) {
log.error("数据库健康检查失败", e);
HealthMonitor.setDatabaseStatus(HealthStatus.DOWN);
// 触发降级逻辑
triggerDegradation();
}
}
private void triggerDegradation() {
// 切换到只读模式
SystemConfig.setReadOnlyMode(true);
// 通知所有服务实例
notifyAllInstances("database_down", System.currentTimeMillis());
// 记录故障事件
FaultEvent event = new FaultEvent();
event.setEventType(FaultType.DATABASE_UNAVAILABLE);
event.setSeverity(Severity.CRITICAL);
event.setOccurTime(new Date());
event.setDescription("数据库连接失败,已切换到只读降级模式");
faultEventService.recordEvent(event);
}
}
在数据一致性保障上,跨机房同步采用“日志同步+定期校验”模式,确保灾难发生时RPO(数据丢失量)近乎为零。
-- 配置逻辑复制
-- 发布端配置
CREATE PUBLICATION card_publication FOR ALL TABLES;
-- 订阅端配置
CREATE SUBSCRIPTION beijing_subscription
CONNECTION 'host=bj-db-host port=5432 dbname=carddb user=replicator password=xxxxxx'
PUBLICATION card_publication
WITH (
copy_data = true,
create_slot = true,
enabled = true,
slot_name = 'beijing_slot'
);
-- 监控复制延迟
SELECT
client_addr,
application_name,
state,
sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), write_lsn) AS write_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS flush_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag
FROM pg_stat_replication;
// 缓存服务实现
@Service
public class CacheServiceImpl implements CacheService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 防缓存穿透查询
*/
@Override
public T queryWithPenetrationProtection(String key, Class clazz,
Supplier dbSupplier, Duration ttl) {
// 先查缓存
T value = (T) redisTemplate.opsForValue().get(key);
if (value != null) {
// 空值标记处理
if (value instanceof NullValue) {
return null;
}
return value;
}
// 使用分布式锁防止缓存击穿
String lockKey = "cache:lock:" + key;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,等待100ms,锁持有时间5s
boolean locked = lock.tryLock(100, 5000, TimeUnit.MILLISECONDS);
if (locked) {
// 再次检查缓存(双重检查)
value = (T) redisTemplate.opsForValue().get(key);
if (value != null) {
if (value instanceof NullValue) {
return null;
}
return value;
}
// 查询数据库
value = dbSupplier.get();
// 写入缓存
if (value == null) {
// 空值缓存,防止缓存穿透
redisTemplate.opsForValue().set(key, NullValue.INSTANCE,
Duration.ofMinutes(5));
} else {
redisTemplate.opsForValue().set(key, value, ttl);
}
return value;
} else {
// 未获取到锁,等待后重试或返回默认值
Thread.sleep(50);
return queryWithPenetrationProtection(key, clazz, dbSupplier, ttl);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 批量查询优化
*/
@Override
public Map batchQuery(List keys, Class clazz,
Function, Map> dbSupplier) {
if (keys.isEmpty()) {
return Collections.emptyMap();
}
// 批量查询缓存
List cachedValues = redisTemplate.opsForValue().multiGet(keys);
Map result = new HashMap<>();
List missingKeys = new ArrayList<>();
// 处理缓存结果
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
Object value = cachedValues.get(i);
if (value == null) {
missingKeys.add(key);
} else if (value instanceof NullValue) {
// 空值标记,不加入结果
continue;
} else {
result.put(key, (T) value);
}
}
// 如果有缺失的key,查询数据库
if (!missingKeys.isEmpty()) {
Map dbResult = dbSupplier.apply(missingKeys);
result.putAll(dbResult);
// 批量写入缓存
Map cacheMap = new HashMap<>();
for (String key : missingKeys) {
T value = dbResult.get(key);
if (value == null) {
cacheMap.put(key, NullValue.INSTANCE);
} else {
cacheMap.put(key, value);
}
}
if (!cacheMap.isEmpty()) {
redisTemplate.opsForValue().multiSet(cacheMap);
// 设置过期时间
for (String key : cacheMap.keySet()) {
if (cacheMap.get(key) instanceof NullValue) {
redisTemplate.expire(key, Duration.ofMinutes(5));
} else {
redisTemplate.expire(key, Duration.ofMinutes(30));
}
}
}
}
return
[AFFILIATE_SLOT_2]
六、核心功能模块代码实践 理论需与实践结合。以下是三个核心模块的关键代码示例,展示了在分布式架构下的数据操作逻辑。
用户管理模块 :负责用户全生命周期管理。
// UserController.java
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* 新增用户
*/
@PostMapping("/add")
public Result addUser(@Valid @RequestBody UserDTO userDTO) {
try {
UserVO userVO = userService.addUser(userDTO);
return Result.success(userVO);
} catch (BusinessException e) {
log.error("新增用户失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 删除用户(逻辑删除)
*/
@PostMapping("/delete/{userId}")
public Result deleteUser(@PathVariable Long userId) {
try {
userService.deleteUser(userId);
return Result.success();
} catch (BusinessException e) {
log.error("删除用户失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 更新用户信息
*/
@PostMapping("/update")
public Result updateUser(@Valid @RequestBody UserUpdateDTO updateDTO) {
try {
UserVO userVO = userService.updateUser(updateDTO);
return Result.success(userVO);
} catch (BusinessException e) {
log.error("更新用户失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 查询用户信息
*/
@GetMapping("/query/{userId}")
public Result queryUser(@PathVariable Long userId) {
try {
UserVO userVO = userService.queryUser(userId);
return Result.success(userVO);
} catch (BusinessException e) {
log.error("查询用户失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 分页查询用户列表
*/
@GetMapping("/list")
public Result> listUsers(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String userName,
@RequestParam(required = false) Integer userType,
@RequestParam(required = false) Integer status) {
UserQueryDTO queryDTO = new UserQueryDTO();
queryDTO.setPageNum(pageNum);
queryDTO.setPageSize(pageSize);
queryDTO.setUserName(userName);
queryDTO.setUserType(userType);
queryDTO.setStatus(status);
PageResult pageResult = userService.listUsers(queryDTO);
return Result.success(pageResult);
}
}
// UserServiceImpl.java
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private CardMapper cardMapper;
@Autowired
private RedisTemplate redisTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public UserVO addUser(UserDTO userDTO) {
// 校验身份证号唯一性
UserInfo existingUser = userMapper.selectByIdCard(userDTO.getIdCard());
if (existingUser != null) {
throw new BusinessException(ErrorCode.USER_ID_CARD_EXIST);
}
// 创建用户信息
UserInfo userInfo = new UserInfo();
BeanUtils.copyProperties(userDTO, userInfo);
userInfo.setStatus(UserStatus.NORMAL.getCode());
userInfo.setCreateTime(new Date());
userInfo.setUpdateTime(new Date());
userMapper.insert(userInfo);
// 创建默认卡片
CardInfo cardInfo = createDefaultCard(userInfo.getUserId());
cardMapper.insert(cardInfo);
// 缓存用户信息
cacheUserInfo(userInfo);
return convertToVO(userInfo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteUser(Long userId) {
UserInfo userInfo = userMapper.selectById(userId);
if (userInfo == null) {
throw new BusinessException(ErrorCode.USER_NOT_EXIST);
}
// 逻辑删除,更新状态为注销
userInfo.setStatus(UserStatus.DELETED.getCode());
userInfo.setUpdateTime(new Date());
userMapper.updateById(userInfo);
// 同步更新卡片状态
cardMapper.updateStatusByUserId(userId, CardStatus.DELETED.getCode());
// 清除缓存
clearUserCache(userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public UserVO updateUser(UserUpdateDTO updateDTO) {
UserInfo userInfo = userMapper.selectById(updateDTO.getUserId());
if (userInfo == null) {
throw new BusinessException(ErrorCode.USER_NOT_EXIST);
}
// 更新用户信息
if (StringUtils.isNotBlank(updateDTO.getPhone())) {
userInfo.setPhone(updateDTO.getPhone());
}
if (StringUtils.isNotBlank(updateDTO.getEmail())) {
userInfo.setEmail(updateDTO.getEmail());
}
userInfo.setUpdateTime(new Date());
userMapper.updateById(userInfo);
// 更新缓存
cacheUserInfo(userInfo);
return convertToVO(userInfo);
}
@Override
public UserVO queryUser(Long userId) {
// 先查缓存
String cacheKey = "user:info:" + userId;
UserVO cachedUser = (UserVO) redisTemplate.opsForValue().get(cacheKey);
if (cachedUser != null) {
return cachedUser;
}
// 缓存未命中,查询数据库
UserInfo userInfo = userMapper.selectById(userId);
if (userInfo == null) {
throw new BusinessException(ErrorCode.USER_NOT_EXIST);
}
UserVO userVO = convertToVO(userInfo);
// 写入缓存
redisTemplate.opsForValue().set(cacheKey, userVO, 30, TimeUnit.MINUTES);
return userVO;
}
@Override
public PageResult listUsers(UserQueryDTO queryDTO) {
PageHelper.startPage(queryDTO.getPageNum(), queryDTO.getPageSize());
List userList = userMapper.selectByCondition(queryDTO);
PageInfo pageInfo = new PageInfo<>(userList);
List voList = userList.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return new PageResult<>(
voList,
pageInfo.getTotal(),
pageInfo.getPageNum(),
pageInfo.getPageSize()
);
}
private void cacheUserInfo(UserInfo userInfo) {
String cacheKey = "user:info:" + userInfo.getUserId();
UserVO userVO = convertToVO(userInfo);
redisTemplate.opsForValue().set(cacheKey, userVO, 30, TimeUnit.MINUTES);
}
private void clearUserCache(Long userId) {
String cacheKey = "user:info:" + userId;
redisTemplate.delete(cacheKey);
}
}
交易处理模块 :系统的核心,必须保证资金操作的原子性与一致性。
// TransactionController.java
@RestController
@RequestMapping("/api/transaction")
@Slf4j
public class TransactionController {
@Autowired
private TransactionService transactionService;
/**
* 消费扣款
*/
@PostMapping("/consume")
public Result consume(@Valid @RequestBody ConsumeDTO consumeDTO) {
try {
TransactionVO transactionVO = transactionService.consume(consumeDTO);
return Result.success(transactionVO);
} catch (BusinessException e) {
log.error("消费扣款失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 账户充值
*/
@PostMapping("/recharge")
public Result recharge(@Valid @RequestBody RechargeDTO rechargeDTO) {
try {
TransactionVO transactionVO = transactionService.recharge(rechargeDTO);
return Result.success(transactionVO);
} catch (BusinessException e) {
log.error("账户充值失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 查询交易详情
*/
@GetMapping("/detail/{transId}")
public Result getDetail(@PathVariable Long transId) {
try {
TransactionVO transactionVO = transactionService.getDetail(transId);
return Result.success(transactionVO);
} catch (BusinessException e) {
log.error("查询交易详情失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 分页查询交易流水
*/
@GetMapping("/list")
public Result> listTransactions(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "20") Integer pageSize,
@RequestParam(required = false) String cardId,
@RequestParam(required = false) Integer transType,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime) {
TransactionQueryDTO queryDTO = new TransactionQueryDTO();
queryDTO.setPageNum(pageNum);
queryDTO.setPageSize(pageSize);
queryDTO.setCardId(cardId);
queryDTO.setTransType(transType);
if (StringUtils.isNotBlank(startTime)) {
queryDTO.setStartTime(DateUtil.parse(startTime));
}
if (StringUtils.isNotBlank(endTime)) {
queryDTO.setEndTime(DateUtil.parse(endTime));
}
PageResult pageResult = transactionService.listTransactions(queryDTO);
return Result.success(pageResult);
}
/**
* 交易冲正(异常处理)
*/
@PostMapping("/reverse/{transId}")
public Result reverseTransaction(@PathVariable Long transId) {
try {
transactionService.reverseTransaction(transId);
return Result.success();
} catch (BusinessException e) {
log.error("交易冲正失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
}
// TransactionServiceImpl.java
@Service
@Slf4j
public class TransactionServiceImpl implements TransactionService {
@Autowired
private TransactionMapper transactionMapper;
@Autowired
private CardMapper cardMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public TransactionVO consume(ConsumeDTO consumeDTO) {
// 获取卡片信息
CardInfo cardInfo = cardMapper.selectByCardId(consumeDTO.getCardId());
if (cardInfo == null) {
throw new BusinessException(ErrorCode.CARD_NOT_EXIST);
}
// 校验卡片状态
if (cardInfo.getCardStatus() != CardStatus.NORMAL.getCode()) {
throw new BusinessException(ErrorCode.CARD_STATUS_ABNORMAL);
}
// 校验消费限额
validateConsumeLimit(cardInfo, consumeDTO.getAmount());
// 使用分布式锁保证并发安全
String lockKey = "card:consume:lock:" + consumeDTO.getCardId();
RLock lock = redissonClient.getLock(lockKey);
try {
boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException(ErrorCode.SYSTEM_BUSY);
}
// 扣减余额
BigDecimal beforeBalance = cardInfo.getBalance();
BigDecimal afterBalance = beforeBalance.subtract(consumeDTO.getAmount());
if (afterBalance.compareTo(BigDecimal.ZERO) < 0) {
throw new BusinessException(ErrorCode.INSUFFICIENT_BALANCE);
}
cardInfo.setBalance(afterBalance);
cardInfo.setLastUsedTime(new Date());
cardMapper.updateBalance(cardInfo);
// 记录交易流水
TransactionLog transactionLog = new TransactionLog();
transactionLog.setCardId(consumeDTO.getCardId());
transactionLog.setTransType(TransType.CONSUME.getCode());
transactionLog.setTransAmount(consumeDTO.getAmount());
transactionLog.setBeforeBalance(beforeBalance);
transactionLog.setAfterBalance(afterBalance);
transactionLog.setTerminalId(consumeDTO.getTerminalId());
transactionLog.setMerchantId(consumeDTO.getMerchantId());
transactionLog.setLocationInfo(consumeDTO.getLocationInfo());
transactionLog.setTransTime(new Date());
transactionLog.setStatus(TransStatus.SUCCESS.getCode());
transactionMapper.insert(transactionLog);
// 更新缓存
updateCardCache(cardInfo);
// 发送交易通知(异步)
sendTransactionNotification(transactionLog);
return convertToVO(transactionLog);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public TransactionVO recharge(RechargeDTO rechargeDTO) {
// 获取卡片信息
CardInfo cardInfo = cardMapper.selectByCardId(rechargeDTO.getCardId());
if (cardInfo == null) {
throw new BusinessException(ErrorCode.CARD_NOT_EXIST);
}
// 校验充值渠道
validateRechargeChannel(rechargeDTO.getChannel());
// 增加余额
BigDecimal beforeBalance = cardInfo.getBalance();
BigDecimal afterBalance = beforeBalance.add(rechargeDTO.getAmount());
cardInfo.setBalance(afterBalance);
cardMapper.updateBalance(cardInfo);
// 记录交易流水
TransactionLog transactionLog = new TransactionLog();
transactionLog.setCardId(rechargeDTO.getCardId());
transactionLog.setTransType(TransType.RECHARGE.getCode());
transactionLog.setTransAmount(rechargeDTO.getAmount());
transactionLog.setBeforeBalance(beforeBalance);
transactionLog.setAfterBalance(afterBalance);
transactionLog.setTerminalId(rechargeDTO.getTerminalId());
transactionLog.setChannel(rechargeDTO.getChannel());
transactionLog.setChannelOrderNo(rechargeDTO.getChannelOrderNo());
transactionLog.setTransTime(new Date());
transactionLog.setStatus(TransStatus.SUCCESS.getCode());
transactionMapper.insert(transactionLog);
// 更新缓存
updateCardCache(cardInfo);
// 发送充值成功通知
sendRechargeNotification(transactionLog);
return convertToVO(transactionLog);
}
@Override
public TransactionVO getDetail(Long transId) {
// 先查缓存
String cacheKey = "transaction:detail:" + transId;
TransactionVO cachedTrans = (TransactionVO) redisTemplate.opsForValue().get(cacheKey);
if (cachedTrans != null) {
return cachedTrans;
}
// 查询数据库
TransactionLog transactionLog = transactionMapper.selectById(transId);
if (transactionLog == null) {
throw new BusinessException(ErrorCode.TRANSACTION_NOT_EXIST);
}
TransactionVO transactionVO = convertToVO(transactionLog);
// 写入缓存
redisTemplate.opsForValue().set(cacheKey, transactionVO, 10, TimeUnit.MINUTES);
return transactionVO;
}
@Override
public PageResult listTransactions(TransactionQueryDTO queryDTO) {
PageHelper.startPage(queryDTO.getPageNum(), queryDTO.getPageSize());
List transactionList = transactionMapper.selectByCondition(queryDTO);
PageInfo pageInfo = new PageInfo<>(transactionList);
List voList = transactionList.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return new PageResult<>(
voList,
pageInfo.getTotal(),
pageInfo.getPageNum(),
pageInfo.getPageSize()
);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void reverseTransaction(Long transId) {
TransactionLog originalTrans = transactionMapper.selectById(transId);
if (originalTrans == null) {
throw new BusinessException(ErrorCode.TRANSACTION_NOT_EXIST);
}
// 校验是否可冲正
if (!canReverse(originalTrans)) {
throw new BusinessException(ErrorCode.TRANSACTION_CANNOT_REVERSE);
}
// 获取卡片信息
CardInfo cardInfo = cardMapper.selectByCardId(originalTrans.getCardId());
// 恢复余额
BigDecimal currentBalance = cardInfo.getBalance();
BigDecimal reversedBalance = currentBalance.add(originalTrans.getTransAmount());
cardInfo.setBalance(reversedBalance);
cardMapper.updateBalance(cardInfo);
// 记录冲正流水
TransactionLog reverseLog = new TransactionLog();
reverseLog.setCardId(originalTrans.getCardId());
reverseLog.setTransType(TransType.REVERSE.getCode());
reverseLog.setTransAmount(originalTrans.getTransAmount());
reverseLog.setBeforeBalance(currentBalance);
reverseLog.setAfterBalance(reversedBalance);
reverseLog.setRelatedTransId(transId);
reverseLog.setTransTime(new Date());
reverseLog.setStatus(TransStatus.SUCCESS.getCode());
reverseLog.setRemark("冲正交易,原交易ID:" + transId);
transactionMapper.insert(reverseLog);
// 更新原交易状态
originalTrans.setStatus(TransStatus.REVERSED.getCode());
transactionMapper.updateStatus(originalTrans);
// 更新缓存
updateCardCache(cardInfo);
clearTransactionCache(transId);
}
private void validateConsumeLimit(CardInfo cardInfo, BigDecimal amount) {
// 校验单笔限额
if (cardInfo.getSingleLimit() != null && amount.compareTo(cardInfo.getSingleLimit()) > 0) {
throw new BusinessException(ErrorCode.EXCEED_SINGLE_LIMIT);
}
// 校验当日累计消费(从Redis获取)
String dailyKey = "card:daily:consume:" + cardInfo.getCardId() + ":" + DateUtil.today();
BigDecimal dailyConsume = (BigDecimal) redisTemplate.opsForValue().get(dailyKey);
if (dailyConsume == null) {
dailyConsume = BigDecimal.ZERO;
}
BigDecimal afterDaily = dailyConsume.add(amount);
if (cardInfo.getDailyLimit() != null && afterDaily.compareTo(cardInfo.getDailyLimit()) > 0) {
throw new BusinessException(ErrorCode.EXCEED_DAILY_LIMIT);
}
}
}
清分结算模块 :涉及财务对账,对准确性和事务性要求极高。
// SettlementController.java
@RestController
@RequestMapping("/api/settlement")
@Slf4j
public class SettlementController {
@Autowired
private SettlementService settlementService;
/**
* 生成日终结算单
*/
@PostMapping("/daily/generate")
public Result generateDailySettlement(@RequestParam String settleDate) {
try {
SettlementVO settlementVO = settlementService.generateDailySettlement(settleDate);
return Result.success(settlementVO);
} catch (BusinessException e) {
log.error("生成日终结算单失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 查询结算单详情
*/
@GetMapping("/detail/{settleId}")
public Result getSettlementDetail(@PathVariable Long settleId) {
try {
SettlementDetailVO detailVO = settlementService.getSettlementDetail(settleId);
return Result.success(detailVO);
} catch (BusinessException e) {
log.error("查询结算单详情失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 确认结算单
*/
@PostMapping("/confirm/{settleId}")
public Result confirmSettlement(@PathVariable Long settleId) {
try {
settlementService.confirmSettlement(settleId);
return Result.success();
} catch (BusinessException e) {
log.error("确认结算单失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
/**
* 分页查询结算单列表
*/
@GetMapping("/list")
public Result> listSettlements(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "20") Integer pageSize,
@RequestParam(required = false) String merchantId,
@RequestParam(required = false) Integer settleStatus,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
SettlementQueryDTO queryDTO = new SettlementQueryDTO();
queryDTO.setPageNum(pageNum);
queryDTO.setPageSize(pageSize);
queryDTO.setMerchantId(merchantId);
queryDTO.setSettleStatus(settleStatus);
queryDTO.setStartDate(startDate);
queryDTO.setEndDate(endDate);
PageResult pageResult = settlementService.listSettlements(queryDTO);
return Result.success(pageResult);
}
/**
* 删除结算单(仅限草稿状态)
*/
@PostMapping("/delete/{settleId}")
public Result deleteSettlement(@PathVariable Long settleId) {
try {
settlementService.deleteSettlement(settleId);
return Result.success();
} catch (BusinessException e) {
log.error("删除结算单失败: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
}
}
// SettlementServiceImpl.java
@Service
@Slf4j
public class SettlementServiceImpl implements SettlementService {
@Autowired
private SettlementMapper settlementMapper;
@Autowired
private SettlementDetailMapper settlementDetailMapper;
@Autowired
private TransactionMapper transactionMapper;
@Autowired
private MerchantMapper merchantMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public SettlementVO generateDailySettlement(String settleDate) {
// 校验日期格式
if (!DateUtil.isValidDate(settleDate)) {
throw new BusinessException(ErrorCode.INVALID_DATE_FORMAT);
}
// 检查是否已生成过结算单
Settlement existing = settlementMapper.selectBySettleDate(settleDate);
if (existing != null) {
throw new BusinessException(ErrorCode.SETTLEMENT_ALREADY_EXISTS);
}
// 获取所有商户
List merchantList = merchantMapper.selectAllActive();
// 创建结算主单
Settlement settlement = new Settlement();
settlement.setSettleDate(settleDate);
settlement.setSettleStatus(SettleStatus.DRAFT.getCode());
settlement.setTotalAmount(BigDecimal.ZERO);
settlement.setTotalFee(BigDecimal.ZERO);
settlement.setSettleAmount(BigDecimal.ZERO);
settlement.setCreateTime(new Date());
settlementMapper.insert(settlement);
List detailList = new ArrayList<>();
BigDecimal totalAmount = BigDecimal.ZERO;
BigDecimal totalFee = BigDecimal.ZERO;
// 为每个商户生成结算明细
for (MerchantInfo merchant : merchantList) {
// 查询商户当日交易汇总
TransactionSummary summary = transactionMapper.selectDailySummary(
merchant.getMerchantId(), settleDate);
if (summary == null || summary.getTotalAmount().compareTo(BigDecimal.ZERO) == 0) {
continue;
}
// 计算手续费
BigDecimal fee = calculateFee(summary.getTotalAmount(), merchant.getSettlementRate());
BigDecimal settleAmount = summary.getTotalAmount().subtract(fee);
// 创建结算明细
SettlementDetail detail = new SettlementDetail();
detail.setSettleId(settlement.getSettleId());
detail.setMerchantId(merchant.getMerchantId());
detail.setMerchantName(merchant.getMerchantName());
detail.setTransCount(summary.getTransCount());
detail.setTotalAmount(summary.getTotalAmount());
detail.setFeeAmount(fee);
detail.setSettleAmount(settleAmount);
detail.setSettleStatus(SettleStatus.DRAFT.getCode());
detail.setCreateTime(new Date());
settlementDetailMapper.insert(detail);
detailList.add(detail);
// 累加总计
totalAmount = totalAmount.add(summary.getTotalAmount());
totalFee = totalFee.add(fee);
}
// 更新结算主单
settlement.setTotalAmount(totalAmount);
settlement.setTotalFee(totalFee);
settlement.setSettleAmount(totalAmount.subtract(totalFee));
settlement.setDetailCount(detailList.size());
settlement.setUpdateTime(new Date());
settlementMapper.updateById(settlement);
// 发送结算单生成通知
sendSettlementGeneratedNotification(settlement);
return convertToVO(settlement);
}
@Override
public SettlementDetailVO getSettlementDetail(Long settleId) {
// 查询结算主单
Settlement settlement = settlementMapper.selectById(settleId);
if (settlement == null) {
throw new BusinessException(ErrorCode.SETTLEMENT_NOT_EXIST);
}
// 查询结算明细
List detailList = settlementDetailMapper.selectBySettleId(settleId);
// 转换为VO
SettlementDetailVO detailVO = new SettlementDetailVO();
detailVO.setSettlement(convertToVO(settlement));
detailVO.setDetails(detailList.stream()
.map(this::convertDetailToVO)
.collect(Collectors.toList()));
return detailVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void confirmSettlement(Long settleId) {
Settlement settlement = settlementMapper.selectById(settleId);
if (settlement == null) {
throw new BusinessException(ErrorCode.SETTLEMENT_NOT_EXIST);
}
// 校验状态
if (settlement.getSettleStatus() != SettleStatus.DRAFT.getCode()) {
throw new BusinessException(ErrorCode.SETTLEMENT_STATUS_ERROR);
}
// 更新结算单状态
settlement.setSettleStatus(SettleStatus.CONFIRMED.getCode());
settlement.setConfirmTime(new Date());
settlement.setUpdateTime(new Date());
settlementMapper.updateById(settlement);
// 更新明细状态
settlementDetailMapper.updateStatusBySettleId(settleId, SettleStatus.CONFIRMED.getCode());
// 触发资金划拨(异步)
triggerFundTransfer(settlement);
// 发送结算确认通知
sendSettlementConfirmedNotification(settlement);
}
@Override
public PageResult listSettlements(SettlementQueryDTO queryDTO) {
PageHelper.startPage(queryDTO.getPageNum(), queryDTO.getPageSize());
List settlementList = settlementMapper.selectByCondition(queryDTO);
PageInfo pageInfo = new PageInfo<>(settlementList);
List voList = settlementList.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
return new PageResult<>(
voList,
pageInfo.getTotal(),
pageInfo.getPageNum(),
pageInfo.getPageSize()
);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteSettlement(Long settleId) {
Settlement settlement = settlementMapper.selectById(settleId);
if (settlement == null) {
throw new BusinessException(ErrorCode.SETTLEMENT_NOT_EXIST);
}
// 仅允许删除草稿状态的结算单
if (settlement.getSettleStatus() != SettleStatus.DRAFT.getCode()) {
throw new BusinessException(ErrorCode.SETTLEMENT_CANNOT_DELETE);
}
// 删除结算明细
settlementDetailMapper.deleteBySettleId(settleId);
// 删除结算主单
settlementMapper.deleteById(settleId);
// 发送删除通知
sendSettlementDeletedNotification(settlement);
}
private BigDecimal calculateFee(BigDecimal amount, BigDecimal rate) {
if (rate == null || rate.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
// 四舍五入,保留2位小数
return amount.multiply(rate).setScale(2, RoundingMode.HALF_UP);
}
}
结语 本次一卡通系统的国产化改造实践充分证明,通过分布式数据库架构 、精细化的性能调优、结合容器化部署 与Kubernetes编排 的弹性伸缩能力,完全能够基于国产技术栈构建出支撑亿级用户、高并发交易的核心业务系统。这不仅实现了技术自主可控的突破,更在性能、稳定性和运维效率上获得了显著提升。随着信创生态的成熟,这套融合了现代云原生理念的数据库架构方案,将为更多关键行业的数字化转型提供坚实且可靠的底层支撑。