1. 项目概述
这个共享单车信息系统是一个典型的互联网出行解决方案,采用前后端分离架构设计。系统主要解决城市共享单车的智能化管理问题,包括车辆定位、用户骑行、费用结算、运维调度等核心业务场景。我在实际开发中发现,这类系统需要特别关注高并发处理和数据一致性,因为早晚高峰时段的用车请求往往呈现爆发式增长。
技术栈选择上,后端采用Spring Boot 2.7.x版本,前端使用Vue 3组合式API,数据库采用MySQL 8.0。这套技术组合在保证系统性能的同时,也兼顾了开发效率和可维护性。特别值得一提的是,我们通过Spring Cloud Alibaba组件实现了服务的弹性扩展,这在应对突发流量时表现尤为出色。
2. 系统架构设计
2.1 整体架构解析
系统采用经典的三层架构,但针对共享单车场景做了特殊优化:
code复制表现层(Vue 3 + Element Plus)
↓ HTTP/WebSocket
业务逻辑层(Spring Boot + Spring Cloud)
↓ JDBC/RPC
数据访问层(MySQL + Redis + Elasticsearch)
这种架构设计有三大优势:
- 前后端完全解耦,允许移动端和Web端并行开发
- 引入Redis缓存热点数据(如车辆实时位置),将查询性能提升5-8倍
- 通过Elasticsearch实现地理空间查询,支持3公里内单车秒级检索
2.2 数据库设计要点
共享单车系统的数据库设计有几个关键考量:
车辆表(bike)主要字段:
sql复制CREATE TABLE `bike` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`bike_no` varchar(20) NOT NULL COMMENT '车辆编号',
`type` tinyint NOT NULL COMMENT '车型 1-普通 2-电动',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 1-可用 2-使用中 3-维修中',
`gps_point` point NOT NULL COMMENT '当前位置坐标',
`battery` int DEFAULT NULL COMMENT '电量(电动车型)',
`lock_status` tinyint NOT NULL DEFAULT '0' COMMENT '锁状态 0-锁定 1-解锁',
PRIMARY KEY (`id`),
SPATIAL KEY `idx_gps` (`gps_point`),
UNIQUE KEY `uk_bike_no` (`bike_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别注意:
- 使用MySQL的空间索引(SPATIAL INDEX)加速地理位置查询
- 车辆编号建立唯一约束防止重复录入
- 状态字段使用tinyint而非enum,便于后期扩展
3. 核心功能实现
3.1 单车解锁流程
这是系统最核心的交互流程,涉及多个服务的协同:
java复制// BikeServiceImpl.java
@Transactional
public UnlockResult unlockBike(Long userId, String bikeNo) {
// 1. 校验用户状态
User user = userService.getById(userId);
if (user.getStatus() != UserStatus.NORMAL) {
throw new BusinessException("用户状态异常");
}
// 2. 获取车辆信息(带乐观锁)
Bike bike = bikeMapper.selectByNoForUpdate(bikeNo);
if (bike.getStatus() != BikeStatus.AVAILABLE) {
throw new BusinessException("车辆不可用");
}
// 3. 创建骑行记录
Trip trip = new Trip();
trip.setUserId(userId);
trip.setBikeId(bike.getId());
trip.setStartTime(LocalDateTime.now());
tripMapper.insert(trip);
// 4. 更新车辆状态
bike.setStatus(BikeStatus.IN_USE);
bike.setLockStatus(LockStatus.UNLOCKED);
bikeMapper.updateById(bike);
// 5. 调用IoT服务开锁
iotService.sendUnlockCommand(bike.getDeviceId());
return new UnlockResult(trip.getId(), bike.getType());
}
关键点说明:
- 使用
@Transactional保证业务原子性 selectForUpdate防止并发修改- 操作顺序严格遵循:校验→创建记录→改状态→发指令
- 异常时会自动回滚事务
3.2 位置更新策略
车辆位置信息需要高频更新,我们采用分级处理:
-
实时位置(每15秒更新):
- 存入Redis Geo结构
- 键名:
bike:location:${cityCode} - 命令:
GEOADD bike:location:010 116.404 39.915 bike_1001
-
轨迹记录(每分钟归档):
- 批量写入MySQL轨迹表
- 使用LOAD DATA INFILE提升批量插入性能
-
历史数据分析:
- 每日凌晨跑批计算热点区域
- 生成车辆调度建议
4. 性能优化实践
4.1 缓存设计
我们采用多级缓存策略:
-
本地缓存(Caffeine):
- 缓存静态数据:车型配置、计费规则
- 有效期5分钟,最大条目1000
-
Redis缓存:
- 热点数据:用户信息、车辆状态
- 数据结构优化:
java复制// 用户行程使用Hash存储 redisTemplate.opsForHash().put( "user:trip:" + userId, tripId, JSON.toJSONString(tripInfo) ); // 附近车辆使用Geo redisTemplate.opsForGeo().add( "bike:location:010", new Point(116.404, 39.915), "bike_1001" );
4.2 数据库优化
-
读写分离:
- 写操作走主库
- 读操作随机访问从库
- 使用ShardingSphere实现透明路由
-
分表策略:
- 骑行记录按月分表(trip_202301)
- 使用MyBatis拦截器自动路由
-
索引优化:
- 联合索引遵循最左匹配原则
- 为高频查询添加覆盖索引
sql复制ALTER TABLE trip ADD INDEX idx_user_time (user_id, start_time)
5. 安全防护措施
5.1 接口安全
-
防重放攻击:
- 每个请求必须包含nonce随机串
- 服务端缓存最近5分钟的nonce
-
参数校验:
java复制@GetMapping("/nearby") public List<Bike> findNearby( @RequestParam @DecimalMin("73.66") @DecimalMax("135.05") Double lng, @RequestParam @DecimalMin("3.86") @DecimalMax("53.55") Double lat, @RequestParam @Max(5000) Integer radius) { // 业务逻辑 } -
SQL注入防护:
- 强制使用预编译语句
- 禁止拼接SQL
- 使用MyBatis等ORM框架
5.2 支付安全
-
金额校验:
- 前端显示金额与后端实际扣款分离
- 支付时二次确认
-
防并发扣款:
java复制@Transactional public void deductBalance(Long userId, BigDecimal amount) { // 使用CAS更新 int rows = userMapper.updateBalance( userId, amount, LocalDateTime.now()); if (rows == 0) { throw new BusinessException("余额不足"); } }
6. 异常处理方案
6.1 分布式事务
对于跨服务操作(如:解锁车辆+创建订单),我们采用:
-
最终一致性方案:
- 本地事务+消息队列
- 使用RocketMQ事务消息
-
补偿机制:
java复制// 解锁失败补偿逻辑 @Transactional public void compensateUnlock(Long tripId) { Trip trip = tripMapper.selectById(tripId); if (trip.getStatus() == TripStatus.STARTED) { bikeMapper.updateStatus(trip.getBikeId(), BikeStatus.AVAILABLE); tripMapper.updateStatus(tripId, TripStatus.CANCELED); } }
6.2 熔断降级
使用Sentinel实现:
-
关键接口配置熔断规则:
java复制@SentinelResource( value = "queryBikeInfo", blockHandler = "handleBlock", fallback = "handleFallback") public BikeVO queryBikeInfo(Long bikeId) { // 业务逻辑 } // 熔断处理 public BikeVO handleBlock(Long bikeId, BlockException ex) { return cacheService.getCachedBike(bikeId); } -
降级策略:
- 返回缓存数据
- 返回兜底值
- 记录日志异步修复
7. 部署架构
7.1 容器化部署
使用Docker Compose编排:
yaml复制version: '3'
services:
app:
image: bike-service:1.0
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=123456
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
mysql_data:
7.2 监控方案
-
指标收集:
- Spring Boot Actuator暴露指标
- Prometheus定时抓取
-
日志收集:
- ELK栈集中管理
- 关键业务日志标记traceId
-
告警规则:
- 接口成功率 < 99.9%
- 平均响应时间 > 500ms
- JVM内存使用 > 80%
8. 开发心得
在实际开发中,有几个经验值得分享:
- 车辆状态管理:
- 使用状态机模式避免非法状态转换
- 定义清晰的状