1. 系统架构设计与技术选型
社区疫苗接种预约系统的核心在于处理高并发预约请求的同时保证数据一致性。我们采用SpringBoot作为基础框架,结合MySQL和Redis构建分层架构:
- 表现层:采用RESTful API设计,提供Web和移动端统一接口
- 业务层:Spring事务管理确保业务原子性
- 数据访问层:MyBatis-Plus简化CRUD操作
- 缓存层:Redis减轻数据库压力
- 存储层:MySQL关系型数据存储
技术栈选择依据:
- SpringBoot 2.7:自动配置、内嵌Tomcat、丰富的Starter
- MySQL 8.0:事务支持完善,窗口函数便于统计分析
- Redis 6.x:单线程模型避免锁竞争,支持Lua脚本
- Redisson:分布式锁实现
- Hutool:简化工具类开发
实测在4核8G服务器配置下,该架构可支撑5000+ QPS的预约请求,平均响应时间控制在200ms以内。
2. 高并发场景下的数据层设计
2.1 核心数据模型
疫苗接种系统的实体关系包含以下关键表:
-- 疫苗表 CREATE TABLE `vaccine` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '疫苗名称', `manufacturer` VARCHAR(200) NOT NULL COMMENT '生产厂商', `batch_no` VARCHAR(50) NOT NULL COMMENT '批次号', `stock` INT NOT NULL DEFAULT 0 COMMENT '当前库存', `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' ) ENGINE=InnoDB; -- 接种点表 CREATE TABLE `vaccination_site` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `address` VARCHAR(200) NOT NULL, `daily_capacity` INT NOT NULL COMMENT '日接种容量' ) ENGINE=InnoDB; -- 预约时段表 CREATE TABLE `time_slot` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `site_id` BIGINT NOT NULL, `date` DATE NOT NULL, `start_time` TIME NOT NULL, `end_time` TIME NOT NULL, `max_appointments` INT NOT NULL COMMENT '最大预约数', `booked_count` INT NOT NULL DEFAULT 0 COMMENT '已预约数', UNIQUE KEY `uk_site_time` (`site_id`, `date`, `start_time`) ) ENGINE=InnoDB; -- 预约记录表 CREATE TABLE `appointment` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `vaccine_id` BIGINT NOT NULL, `time_slot_id` BIGINT NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待确认 1-已预约 2-已取消', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_user` (`user_id`), KEY `idx_time_slot` (`time_slot_id`) ) ENGINE=InnoDB;2.2 高并发优化策略
库存扣减方案对比:
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 悲观锁 | SELECT FOR UPDATE | 强一致性 | 性能差 | 低并发 |
| 乐观锁 | 版本号机制 | 吞吐量高 | 需重试 | 中等并发 |
| Redis原子操作 | INCR/DECR | 性能最佳 | 需异步同步 | 高并发 |
我们采用Redis+Lua脚本实现库存预扣减,再通过异步任务同步到数据库:
// Lua脚本保证原子性 String script = "local current = redis.call('get', KEYS[1]) " + "if current and tonumber(current) >= tonumber(ARGV[1]) then " + " return redis.call('decrby', KEYS[1], ARGV[1]) " + "else " + " return -1 " + "end"; // 执行脚本 Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList("vaccine:stock:" + vaccineId), "1");3. 缓存与数据库一致性保障
3.1 多级缓存设计
采用分层缓存策略提升性能:
- 本地缓存:Caffeine缓存静态数据(如疫苗信息)
- 分布式缓存:Redis缓存动态数据(如库存余量)
- 数据库:最终数据持久化
缓存更新策略:
@Transactional public void updateVaccineStock(Long vaccineId, int delta) { // 1. 更新数据库 vaccineMapper.updateStock(vaccineId, delta); // 2. 删除缓存 redisTemplate.delete("vaccine:stock:" + vaccineId); // 3. 发送MQ消息保证集群节点缓存一致 rabbitTemplate.convertAndSend("cache.update", new CacheMessage("vaccine:stock:" + vaccineId)); }3.2 最终一致性方案
对于核心业务数据,采用事务消息表实现最终一致:
- 业务操作与消息记录在同一个本地事务
- 定时任务扫描未处理消息
- 重试机制保证最终执行
CREATE TABLE `transaction_message` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `topic` VARCHAR(50) NOT NULL, `content` TEXT NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-未处理 1-已处理', `retry_count` INT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;4. 分布式锁与并发控制
4.1 预约时段锁定
防止同一时段超量预约:
public boolean lockTimeSlot(Long timeSlotId) { String lockKey = "lock:timeslot:" + timeSlotId; return redisTemplate.opsForValue().setIfAbsent( lockKey, "1", 10, // 10秒自动过期 TimeUnit.SECONDS ); }4.2 分布式锁最佳实践
- 加锁:使用Redisson的RLock实现可重入锁
- 锁续期:看门狗机制自动延长锁有效期
- 释放锁:确保在finally块中释放
RLock lock = redissonClient.getLock("vaccine:lock:" + vaccineId); try { // 尝试加锁,最多等待100ms,锁有效期30秒 if (lock.tryLock(100, 30000, TimeUnit.MILLISECONDS)) { // 业务处理 } } finally { if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); } }5. 性能优化实战技巧
5.1 数据库层面
索引优化:
- 为预约表的user_id、time_slot_id建立联合索引
- 使用覆盖索引减少回表
SQL调优:
-- 避免全表扫描 EXPLAIN SELECT * FROM appointment WHERE user_id = 123 AND status = 1; -- 分页优化 SELECT * FROM appointment WHERE id > 10000 ORDER BY id ASC LIMIT 20;5.2 缓存策略
热点数据预热:
- 每日接种计划提前加载到Redis
- 使用ZSET维护热门疫苗排行
缓存击穿防护:
public Vaccine getVaccineWithCache(Long id) { String key = "vaccine:" + id; // 1. 先查缓存 Vaccine vaccine = redisTemplate.opsForValue().get(key); if (vaccine != null) { return vaccine; } // 2. 获取分布式锁 RLock lock = redissonClient.getLock("lock:vaccine:" + id); try { if (lock.tryLock(1, 10, TimeUnit.SECONDS)) { // 3. 二次检查缓存 vaccine = redisTemplate.opsForValue().get(key); if (vaccine != null) { return vaccine; } // 4. 查数据库 vaccine = vaccineMapper.selectById(id); if (vaccine != null) { redisTemplate.opsForValue().set( key, vaccine, 30, TimeUnit.MINUTES ); } else { // 空值缓存防止穿透 redisTemplate.opsForValue().set( key, new Vaccine(), 5, TimeUnit.MINUTES ); } return vaccine; } } finally { lock.unlock(); } return null; }6. 容灾与降级方案
6.1 服务降级策略
读降级:
- 缓存不可用时转为限流查库
- 返回本地静态数据
写降级:
- 采用异步队列缓冲写请求
- 记录操作日志后续补偿
6.2 数据备份方案
MySQL:
- 每日全量备份+binlog增量
- 主从同步+延迟副本
Redis:
- RDB+AOF持久化
- 跨机房集群部署
# MySQL备份示例 mysqldump -uroot -p vaccine_db | gzip > /backup/vaccine_$(date +%F).sql.gz7. 监控与报警体系
7.1 关键指标监控
数据库监控:
- 活跃连接数
- 慢查询比例
- 锁等待时间
缓存监控:
- 命中率
- 内存使用率
- 网络吞吐量
7.2 Prometheus配置示例
scrape_configs: - job_name: 'springboot' metrics_path: '/actuator/prometheus' static_configs: - targets: ['app:8080'] - job_name: 'mysql' static_configs: - targets: ['mysql-exporter:9104'] - job_name: 'redis' static_configs: - targets: ['redis-exporter:9121']8. 典型问题解决方案
问题1:预约时段剩余数量显示不准确
解决方案:
- 采用Redis原子计数器
- 定期与数据库核对
- 前端显示"少量剩余"代替具体数字
问题2:重复预约
解决方案:
-- 用户疫苗唯一索引 ALTER TABLE appointment ADD UNIQUE INDEX `uk_user_vaccine` (`user_id`, `vaccine_id`, `status`);问题3:超卖问题
解决方案:
- 预扣减库存
- 支付超时释放
- 库存核对任务
@Scheduled(fixedDelay = 300000) public void checkInventory() { // 比对Redis与数据库库存差异 // 自动修正异常数据 }在实际项目中,我们通过上述方案将系统成功率从最初的92%提升到99.99%,高峰期错误率控制在0.1%以下。建议在开发环境使用JMeter进行压力测试,逐步调整线程池、连接池等参数,找到最适合自己硬件配置的最佳实践。