1. 校园一卡通系统重构背景与技术选型
校园卡系统作为高校信息化建设的核心枢纽,正在经历从单一消费工具向综合服务平台的转型。传统系统普遍存在三个痛点:一是采用C/S架构导致移动端适配困难,二是各业务子系统(如门禁、图书借阅)数据割裂,三是基于Struts等老旧框架开发导致维护成本居高不下。
我们选择SpringBoot作为技术底座主要基于以下考量:
- 快速迭代能力:通过starter依赖和自动配置,能在2周内完成基础框架搭建
- 微服务友好:便于后续拆分用户服务、支付服务等独立模块
- 生态完整性:Spring Security + MyBatis-Plus + Redis的组合能覆盖系统所有技术需求
关键决策:放弃Dubbo而采用Spring Cloud Alibaba进行服务治理,主要考虑到与校园原有Java技术栈的兼容性,以及Nacos配置中心对运维更友好
2. 核心模块设计与实现细节
2.1 高并发账户体系设计
采用"冷热数据分离"策略:
- 热数据(余额、状态)存Redis:读写性能提升10倍
- 全量数据存MySQL:保证数据持久化
- 通过@Cacheable注解实现自动缓存同步
@Service public class CardServiceImpl implements CardService { @Cacheable(value = "card", key = "#cardNumber") public Card getByNumber(String cardNumber) { return cardMapper.selectByNumber(cardNumber); } @CacheEvict(value = "card", key = "#card.number") public void updateCard(Card card) { cardMapper.updateById(card); } }2.2 交易事务处理方案
消费场景需要处理三个关键问题:
- 余额不足时的快速失败
- 分布式环境下的余额一致性问题
- 交易流水可追溯
解决方案:
- 采用本地事务+重试机制
- 交易流水表增加唯一索引防重复
- 使用Spring的@Transactional注解管理事务边界
@Transactional(rollbackFor = Exception.class) public ConsumeResult consume(ConsumeRequest request) { // 1. 校验卡状态 Card card = cardService.getValidCard(request.getCardNumber()); // 2. 检查余额 if(card.getBalance() < request.getAmount()) { return ConsumeResult.fail("余额不足"); } // 3. 扣款 cardService.deductBalance(card.getId(), request.getAmount()); // 4. 记录流水 transactionService.recordTransaction(card, request); return ConsumeResult.success(card.getBalance()); }3. 安全防护体系构建
3.1 认证授权方案对比
| 方案 | 适用场景 | 实现复杂度 | 安全性 |
|---|---|---|---|
| Session-Cookie | 传统Web应用 | 低 | 中 |
| JWT | 前后端分离 | 中 | 高 |
| OAuth2 | 第三方登录 | 高 | 极高 |
最终选择JWT+Spring Security组合:
- 后端无状态便于水平扩展
- 自定义Claim携带用户角色信息
- 通过JJWT库实现签名验证
public class JwtTokenProvider { public String generateToken(UserDetails userDetails) { Claims claims = Jwts.claims().setSubject(userDetails.getUsername()); claims.put("roles", userDetails.getAuthorities()); return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact(); } }3.2 敏感数据保护措施
- 密码存储:PBKDF2WithHmacSHA256算法+随机盐值
- 通信加密:全站HTTPS+HSTS头
- 日志脱敏:通过AOP拦截器自动过滤敏感字段
@Aspect @Component public class DataMaskingAspect { @Around("execution(* com..repository.*.save*(..))") public Object maskSensitiveData(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); if(args[0] instanceof User) { User user = (User) args[0]; user.setIdCard(mask(user.getIdCard())); } return pjp.proceed(args); } private String mask(String value) { // 实现脱敏逻辑 } }4. 性能优化实战记录
4.1 食堂消费高峰应对方案
实测数据显示上午11:30-12:30的QPS可达800+,我们采取三级缓存策略:
- 本地缓存(Caffeine):存储最近10分钟的消费记录
- Redis集群:存所有活跃卡片的余额信息
- MySQL分库分表:按学号尾号分8个库
优化效果对比:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均响应时间 | 450ms | 68ms |
| 错误率 | 1.2% | 0.01% |
| 服务器负载 | 80% | 35% |
4.2 SQL优化典型案例
原始查询(执行时间1.8s):
SELECT * FROM transaction WHERE card_id IN (SELECT id FROM card WHERE status=0) ORDER BY create_time DESC LIMIT 100优化方案:
- 改用JOIN替代子查询
- 为create_time和status字段添加联合索引
- 只查询必要字段
优化后查询(执行时间23ms):
SELECT t.id, t.amount, t.create_time FROM transaction t JOIN card c ON t.card_id=c.id WHERE c.status=0 ORDER BY t.create_time DESC LIMIT 1005. 运维监控体系建设
5.1 健康检查看板配置
通过Spring Boot Actuator暴露关键指标:
management: endpoints: web: exposure: include: health,metrics,prometheus endpoint: health: show-details: always定制健康检查指标:
@Component public class CardServiceHealthIndicator implements HealthIndicator { @Override public Health health() { boolean available = checkDatabaseConnection(); return available ? Health.up().build() : Health.down().withDetail("error", "DB连接超时").build(); } }5.2 日志分析方案
采用ELK栈处理每日10GB+的日志数据:
- Filebeat收集Docker容器日志
- Logstash管道处理:
filter { grok { match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:msg}" } } if [level] == "ERROR" { mutate { add_tag => ["need_alert"] } } }- Kibana展示错误趋势图
6. 踩坑经验与避坑指南
6.1 分布式事务陷阱
初期尝试用Seata处理跨服务交易,发现两个问题:
- 性能下降40%以上
- 与MyBatis-Plus的批量操作存在兼容性问题
最终解决方案:
- 对于强一致性场景:采用本地消息表+定时任务补偿
- 对于弱一致性场景:直接通过RocketMQ事务消息实现
6.2 缓存一致性难题
遇到的典型问题:卡片挂失后,Redis缓存未及时清除导致仍能消费
解决方案链:
- 双写策略:同时更新DB和缓存
- 设置合理的缓存过期时间(5分钟)
- 通过Redis的keyspace通知同步失效缓存
- 增加异步校验任务兜底
@EventListener public void handleCardStatusChange(CardStatusEvent event) { if(event.getNewStatus() == LOST) { redisTemplate.delete("card:" + event.getCardNumber()); } }7. 移动端对接实践
7.1 小程序端关键技术点
- 二维码生成方案:
public String generateQRCode(String cardNumber) { String content = "CARD://" + cardNumber + "?ts=" + System.currentTimeMillis(); return zxingService.generateBase64QR(content, 300, 300); }- 消费通知推送:结合微信模板消息+WebSocket双通道
- 离线模式:允许最后一次成功查询结果缓存24小时
7.2 混合开发优化技巧
- 图片资源压缩:通过TinyPNG API自动压缩上传的证件照
- 接口聚合:将3-5个关联请求合并为单个接口
- 数据差分:仅返回变更字段而非全量数据
优化前后对比:
| 场景 | 原始方案 | 优化方案 |
|---|---|---|
| 首页加载 | 4.2s | 1.1s |
| 月账单查询 | 2.8s | 0.6s |
| 照片上传 | 3.5s | 0.9s |
8. 项目演进路线
8.1 第二阶段规划
- 智能预警系统
- 基于消费模式识别盗刷行为
- 通过Flink实时分析交易流水
- 信用体系构建
- 结合消费记录、图书借阅等数据建立信用分
- 开放信用接口给合作商户
8.2 长期技术储备
- 无感支付探索
- 蓝牙信标定位
- 人脸识别支付
- 区块链应用
- 电子证书存证
- 学分银行体系
这个项目给我的深刻启示是:技术架构必须预留20%-30%的扩展余量。我们最初认为够用的8核16G服务器,在接入门禁系统后CPU长期处于90%负载。后来通过服务拆分和读写分离才解决性能瓶颈