1. 项目背景与核心价值
去年参与某高校学生会选举系统开发时,我深刻体会到传统纸质投票的痛点:计票耗时易错、结果透明度低、异地参与困难。这个基于Spring Boot + MyBatis的在线投票系统,正是为解决这些实际问题而设计的轻量级解决方案。
系统采用前后端分离架构,后端提供RESTful API接口,前端可灵活适配Web或小程序。核心功能包括:多类型投票创建(单选/多选/评分)、实时结果可视化、防刷票机制等。相比市面常见投票工具,我们的特色在于:
- 支持复杂投票逻辑(如条件选项显示)
- 完善的权限控制体系
- 投票数据可追溯审计
2. 技术架构解析
2.1 Spring Boot框架选型
选择Spring Boot 2.7.x版本主要基于:
- 内嵌Tomcat简化部署
- Starter依赖自动配置(特别是Spring Security和Redis)
- Actuator端点监控
- 与MyBatis的天然兼容性
关键配置示例:
@SpringBootApplication @MapperScan("com.vote.mapper") @EnableCaching public class VoteApplication { public static void main(String[] args) { SpringApplication.run(VoteApplication.class, args); } }2.2 MyBatis持久层设计
采用MyBatis-Plus 3.5.x增强功能:
- 动态表名处理器(按年份分表存储投票记录)
- 逻辑删除支持
- 性能分析插件
投票实体关系模型:
CREATE TABLE `vote` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL, `type` tinyint COMMENT '1-单选 2-多选 3-评分', `start_time` datetime NOT NULL, `end_time` datetime NOT NULL, `creator_id` bigint NOT NULL, `is_anonymous` tinyint(1) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3. 核心功能实现
3.1 并发投票控制
采用Redis分布式锁防止重复提交:
public boolean submitVote(Long voteId, Long userId, List<Long> optionIds) { String lockKey = "vote:lock:" + voteId + ":" + userId; try { Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { // 执行投票逻辑 return voteService.processVote(voteId, userId, optionIds); } throw new RuntimeException("操作过于频繁"); } finally { redisTemplate.delete(lockKey); } }3.2 实时统计方案
使用SSE(Server-Sent Events)推送结果更新:
@GetMapping("/results/{voteId}") public SseEmitter streamResults(@PathVariable Long voteId) { SseEmitter emitter = new SseEmitter(3600_000L); resultPushService.addEmitter(voteId, emitter); emitter.onCompletion(() -> resultPushService.removeEmitter(voteId, emitter)); return emitter; }4. 安全防护体系
4.1 防刷票策略
- IP限流(Guava RateLimiter)
- 设备指纹校验
- 验证码二次确认
- 投票行为模式分析
安全配置类示例:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/vote/create").hasRole("ADMIN") .antMatchers("/api/vote/submit").authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }5. 性能优化实践
5.1 缓存策略
- 使用Caffeine实现本地缓存(投票基础信息)
- Redis缓存热门投票结果
- 多级缓存更新策略:
graph LR A[客户端请求] --> B{本地缓存命中?} B -->|是| C[返回缓存] B -->|否| D{Redis缓存命中?} D -->|是| E[更新本地缓存并返回] D -->|否| F[查询数据库并写入缓存]5.2 数据库优化
- 投票记录表按用户ID哈希分片
- 建立复合索引:
ALTER TABLE vote_record ADD INDEX idx_search (vote_id, user_id, submit_time); - 使用MyBatis批量插入:
<insert id="batchInsert" useGeneratedKeys="true"> INSERT INTO vote_record (...) VALUES <foreach collection="list" item="item" separator=","> (#{item.voteId}, #{item.userId}, ...) </foreach> </insert>
6. 部署方案
6.1 容器化部署
Docker Compose编排文件示例:
version: '3' services: vote-app: image: vote-service:1.0 ports: - "8080:8080" depends_on: - redis - mysql redis: image: redis:6-alpine ports: - "6379:6379" mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: rootpass ports: - "3306:3306"6.2 监控配置
Prometheus监控指标示例:
- pattern: /api/vote/submit metrics: - name: vote_submit_total help: Total vote submissions labels: status: $http_status method: $http_method7. 踩坑实录
MyBatis枚举映射问题:
- 现象:投票类型枚举值存入数据库后变成序号
- 解决:配置typeHandlers并指定jdbcType
<result column="type" property="type" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>Redis缓存穿透:
- 现象:高并发查询不存在的投票ID
- 方案:布隆过滤器+空值缓存
public Vote getVoteById(Long id) { if (!bloomFilter.mightContain(id)) { return null; } // ...正常查询逻辑 }SSE连接泄漏:
- 现象:客户端异常断开导致服务端连接未释放
- 修复:添加心跳检测和超时机制
emitter.send(SseEmitter.event() .data("ping") .reconnectTime(30000));
8. 扩展方向
可视化搭建:
- 拖拽式投票表单设计器
- 自定义主题样式模板
智能分析:
- 基于投票结果的用户画像
- 选项关联性分析
多端适配:
- 微信小程序SDK封装
- 桌面端Electron应用
这个项目最让我惊喜的是MyBatis-Plus的动态表名功能,在处理历史投票数据归档时异常好用。建议在复杂查询场景下,可以结合QueryDSL进一步提升代码可读性。