短链接生成系统实战:Spring Boot与Vue前后端分离全流程解析
简介这是基于Spring Boot与Vue框架实现的前后端分离短链接生成管理系统完整项目包包含全部源码与MySQL数据库脚本适合正在学习Spring Boot、Vue、MyBatis等技术或准备课程设计、毕业设计、项目实战的开发者参考。系统区分普通用户与管理员两种角色提供登录认证、首页展示、短链接生成与管理、访问日志记录、统计面板展示等功能并结合ECharts输出操作系统分布饼图、浏览器分布饼图及访问量折线图后台还内置用户管理、角色管理、菜单管理等权限控制模块。压缩包共收录321个文件核心以Vue组件编写前端界面Java后端实现接口逻辑并配合YML/Properties配置文件、SQL数据库脚本以及JavaScript交互脚本辅以SVG图标与SCSS样式另附启动脚本与项目说明文档整体大小仅1.19MB便于快速下载和本地部署。目前已有77人学习下载项目完整度较高可帮助深入理解JWT与Spring Security的认证授权流程、MyBatis与MySQL的数据持久化操作以及前后端分离模式下权限路由与页面交互的实现思路。1. 短链接生成管理系统前后端分离练手的第一步短链接生成管理系统听上去不起眼却是把 Spring Boot、Vue、MySQL 串成一条完整链路的典型项目。它要解决的不只是“把长 URL 变短”而是短码怎么生成才能不重复、跳转用 301 还是 302、过期短链接怎么清理、点击数据从哪来这一串真实工程问题。这套带源码和数据库的项目适合刚学完 Spring Boot 基础、想完整跑一个前后端分离项目的人也适合要快速搭可演示系统的在职开发。你甚至可以把它当模板——短链接只是个外壳里面的表结构、唯一索引、缓存和 Nginx 部署思路换个主题就是一个新项目。2. 项目怎么拆Spring Boot 后端、Vue 前端、MySQL 存储的边界划分2.1 一个项目两个进程目录结构与启动方式前后端分离项目最容易犯的错是把后端的 Controller 当成前端的一部分或者在前端代码里直接拼 SQL。拆这个项目时我习惯先定边界后端只做接口和业务规则前端只管页面渲染和交互MySQL 负责持久化两者通过 JSON 通信。目录上我一般分成 backend、frontend、sql 三个顶层目录short-link-system/ ├── backend/ # Spring Boot 后端 │ ├── src/main/java/com/shortlink │ │ ├── controller/ # 接口层只做参数接收和响应 │ │ ├── service/ # 业务层短码生成、过期判断 │ │ ├── mapper/ # MyBatis 映射接口 │ │ ├── entity/ # 实体类对应表字段 │ │ └── config/ # 跨域、拦截器配置 │ ├── src/main/resources │ │ ├── mapper/ # MyBatis XML 映射文件 │ │ └── application.yml # 端口、数据源配置 │ └── pom.xml ├── frontend/ # Vue 前端 │ ├── src/ │ │ ├── views/ # 页面组件 │ │ ├── api/ # axios 接口封装 │ │ ├── router/ # Vue Router 路由 │ │ └── components/ # 复用组件 │ └── package.json └── sql/ └── short_link.sql # 数据库初始化脚本这里把 mapper 接口和 XML 分开是 MyBatis 最常用的组织方式。新增一个表时按 entity、mapper 接口、XML、service、controller 这个顺序写基本不会漏文件。后端启动直接跑 Application 主类或者用mvn spring-boot:run前端先npm install再npm run dev。开发环境我一般让前端 dev server 监听 3000 端口后端 8080通过代理把 /api 转发到后端避免跨域// vue.config.js module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };参数说明port 是前端页面开发服务器端口target 是后端 Spring Boot 的地址changeOrigin 设为 true 会把请求头里的 Host 改写成 target 地址很多后端框架会校验 Host这一步不能省。配好代理后前端 axios 直接请求/api/shortlink浏览器看到的请求是同源的就不会触发跨域。后端这边数据源和 MyBatis 的核心配置都在 application.yml 里。我一般会写成这样server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/short_link?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 mybatis: mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.shortlink.entity configuration: map-underscore-to-camel-case: true logging: level: com.shortlink.mapper: debug注意几个参数serverTimezoneAsia/Shanghai在 MySQL 8 下不写会直接报时区错误map-underscore-to-camel-case: true让数据库的create_time自动映射成实体的createTime少写一堆 resultMap。characterEncodingutf8配合数据库端的 utf8mb4能存下 emoji 和特殊符号这个坑后面避坑章节会专门说。HikariCP 连接池参数里maximum-pool-size 设 20 对短链接这种短事务项目已经够用压测时如果报Connection is not available优先怀疑这个值太小而不是加服务器。logging.level把 MyBatis 的 mapper 包开到 debugSQL 和入参会打到日志里排查“查不到数据”时非常有用。2.2 数据库表设计短链接表与访问日志表短链接项目核心数据就两张表一张存短码和长链接的对应关系一张存访问日志。建表 SQL 直接从项目数据库脚本里拿出来改一下CREATE DATABASE IF NOT EXISTS short_link DEFAULT CHARACTER SET utf8mb4; USE short_link; -- 短链接主表 CREATE TABLE t_short_link ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 主键, short_code VARCHAR(16) NOT NULL COMMENT 短码例如 a1b2C3, long_url VARCHAR(1024) NOT NULL COMMENT 原始长链接, expire_time DATETIME NULL COMMENT 过期时间NULL 表示永久有效, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 可用0 禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_short_code (short_code) ) ENGINEInnoDB COMMENT短链接主表; -- 访问日志表 CREATE TABLE t_visit_log ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, short_code VARCHAR(16) NOT NULL COMMENT 访问的短码, ip VARCHAR(64) NULL COMMENT 客户端 IP, user_agent VARCHAR(512) NULL COMMENT 浏览器 UA, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_short_code (short_code) ) ENGINEInnoDB COMMENT短链接访问日志;这里最关键的约束是uk_short_code唯一索引。短码碰撞是短链接项目最隐蔽的问题如果只靠代码里先查再插两个并发请求同时查到空结果再插同一条照样重复。唯一索引是最后的闸门冲突时数据库会抛 DuplicateKeyException业务层再决定重试。为什么不用 short_code 当主键字符串主键在频繁插入时索引维护成本比自增 BIGINT 高而且想做关联统计时外键也更啰嗦。自增主键加唯一索引是常规做法。expire_time允许为 NULL 表示永久短码定时任务扫这个字段时只处理非空记录。访问日志表就没那么讲究了一个短码对应多次访问所以不加唯一索引只建普通索引供统计查询用。这张表在项目初期一张表足够真到了每天几千万访问再考虑按天分表或者换时序数据库现在不要过度设计。如果后续要加用户系统只需要在短链接主表加一个user_id字段再补一张用户表别的表不用动。日志表与主表是通过 short_code 关联的因为跳转接口只拿到短码不拿 id。注意建表语句里我故意没写外键。短链接场景里日志表和主表是弱关联外键会影响插入性能而且日志表未来大概率要分表外键到时候全是负担。2.3 跳转数据流从请求到响应的完整走向把目录和表结构定下来后整个系统的数据流就清晰了用户访问http://域名/abc123Nginx 把请求转发给 Spring BootController 根据shortCodeabc123调 ServiceService 先查缓存没有则查 MySQL 的 t_short_link 表拿到 long_url 后设置 302 和 Location 头返回浏览器自动跳转。这个链路里每次访问都往日志表插入一条记录但不影响主流程所以日志写入我一般丢到独立的Async方法里Async public void recordVisit(String shortCode, String ip, String userAgent) { VisitLog log new VisitLog(); log.setShortCode(shortCode); log.setIp(ip); log.setUserAgent(userAgent); visitLogMapper.insert(log); }Async 的代价是启动类要加 EnableAsync否则注解不生效日志写入会同步阻塞跳转接口。这一点属于那种踩了才知道的冷知识代码里有注解但没开关等于没写。3. 短码生成算法从 62 进制转换到冲突处理的完整设计3.1 三种主流短码生成方案对比短链接最核心的算法是短码怎么来。常见做法分成三类各有各的取舍。方案原理优点缺点自增 ID 62 进制把数据库自增 ID 转成 62 进制字符串无碰撞、短码短、实现简单可预测别人能按顺序扫库长 URL 哈希截取对长 URL 做 MD5/SHA 后截 8 位相同 URL 得到相同短码碰撞概率随数据量上升处理复杂随机数 碰撞重试随机生成短码插库失败就重试短码不可预测需要多次查库数据量大时性能差这个项目用的是“自增 ID 62 进制 打乱编码表”的组合。自增 ID 保证不会碰撞转成 62 进制让数字变短再通过打乱字母表让短码不能直接被推算。比如你拿到短码8VTu如果编码表顺序被改过想从短码反推 ID 就没那么容易至少挡一下随手扫描的人。如果你能接受短码变长一点还可以在末尾加一位校验位把生成算法变成“ID 编码 校验字符”代价是长度加一但对防遍历更有效。3.2 用 62 进制做短码Java 实现示例先看一个最基础的 62 进制编码器public class ShortCodeGenerator { // 字母表顺序可以每次部署随机打乱打乱后短码不可预测 private static final String ALPHABET abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789; private static final int BASE ALPHABET.length(); public static String encode(long num) { StringBuilder sb new StringBuilder(); while (num 0) { sb.append(ALPHABET.charAt((int) (num % BASE))); num / BASE; } // 补足 4 位避免太短被撞或被猜 while (sb.length() 4) { sb.append(0); } return sb.reverse().toString(); } public static void main(String[] args) { System.out.println(encode(1)); System.out.println(encode(1024)); System.out.println(encode(100000000L)); } }逻辑说明每次对数字取余 62拿到一个字符然后整除 62直到数字变成 0。这个算法本质就是把十进制换成 62 进制。补位用的字符也是字母表里的字符所以短码定长 4 位起步62 的 6 次方大概 568 亿6 位足够撑到海量数据。ALPHABET为什么不用纯数字加字母顺序因为默认顺序的短码是可逆的别人看到abc就能算出来这是个很小的数字打乱顺序后逆推成本高很多。理论上短码长度不是写死的它由发号器的数字大小决定。如果希望短码长度固定可以在 encode 方法里固定输出位数不足补位、超出就扩容。我一般按 6 位来设计因为 4 位只有 1470 万个组合看起来够用但短链接系统要往长期做6 位更从容。单表记录数到千万级别之前不需要分表真到了再按短码前缀分片。但短码不能只靠编码器还得有一个发号器。常见做法有两种一是用 Redis 的INCR二是用 MySQL 建一张发号表。如果项目不想引入 Redis发号表更合适-- 发号器表只有一行记录 CREATE TABLE t_id_generator ( id INT PRIMARY KEY COMMENT 固定为 1, current_value BIGINT NOT NULL COMMENT 当前已分配的最大 ID ) ENGINEInnoDB; -- 每次取号 UPDATE t_id_generator SET current_value LAST_INSERT_ID(current_value 1) WHERE id 1; SELECT LAST_INSERT_ID();LAST_INSERT_ID(current_value 1)是 MySQL 专门为取号场景设计的用法它把更新后的值赋给LAST_INSERT_ID()同一个会话里再SELECT LAST_INSERT_ID()就能拿到这次分配的数字。这比先查SELECT current_value再UPDATE要稳后者并发下会发重复号。业务侧拿到号之后调用ShortCodeGenerator.encode(id)就得到短码。3.3 冲突处理与过期策略有了发号器理论上不会冲突但代码里仍然要保留冲突兜底因为可能出现手工造数据、多实例发号器配置错误这类意外。我的实现习惯是在插入时依赖数据库唯一索引捕获异常后重试最多重试三次Transactional public ShortLink createShortLink(String longUrl, LocalDateTime expireTime) { for (int i 0; i 3; i) { long id idGenerator.nextId(); String code ShortCodeGenerator.encode(id); ShortLink link new ShortLink(); link.setShortCode(code); link.setLongUrl(longUrl); link.setExpireTime(expireTime); link.setStatus(1); try { shortLinkMapper.insert(link); return link; } catch (DuplicateKeyException e) { // 冲突说明发号器发了重复号或者并发插入了相同短码 log.warn(short code conflict, retry: {}, code); } } throw new RuntimeException(短码生成失败请重试); }这段代码有三个关键点Transactional保证插入失败时事务回滚不脏写重试上限设 3 次避免代码 bug 或发号器故障时无限循环浪费数据库连接catch 的是DuplicateKeyException这是 MyBatis 对 MySQL 唯一索引冲突抛出的异常类型。日志必须打出来因为正常发号器不会走到这一步一旦频繁出现说明发号器逻辑有问题光靠重试是在掩盖事故。过期策略上短链接不像会话数据需要秒级清理。我一般每天跑一次定时任务把过期短码置为禁用而不是删除UPDATE t_short_link SET status 0 WHERE expire_time IS NOT NULL AND expire_time NOW();置为禁用而不是删除为的是保留访问日志和短码的对应关系真删了之后日志表里的 short_code 就变成孤儿数据后续统计会出问题。跳转接口查询时也带上status 1条件过期禁用后自然访问不了。定时任务和查询判断是双保险定时任务负责批量回收查询逻辑负责兜底即时判断避免定时任务没跑时出现过期短码还能跳转。4. 接口与页面落地MyBatis 映射、302 跳转、Vue 联动4.1 MyBatis 映射短码查询与新增的 XML 写法后端 Service 和数据库打交道时MyBatis 的 XML 映射比注解更直观尤其是查询条件多的时候。短链接项目里最核心的两个操作一个按短码查询一个插入新短码mapper namespacecom.shortlink.mapper.ShortLinkMapper select idselectByCode resultTypecom.shortlink.entity.ShortLink SELECT id, short_code, long_url, expire_time, status, create_time FROM t_short_link WHERE short_code #{shortCode} AND status 1 LIMIT 1 /select insert idinsert parameterTypecom.shortlink.entity.ShortLink useGeneratedKeystrue keyPropertyid INSERT INTO t_short_link (short_code, long_url, expire_time, status) VALUES (#{shortCode}, #{longUrl}, #{expireTime}, #{status}) /insert /mapperresultType直接写实体类配合前面配置的map-underscore-to-camel-case数据库的下划线字段会自动映射成驼峰属性不用手写 resultMap。LIMIT 1不是可有可无的万一有脏数据出现重复短码至少不会因为查出来多条直接报错。useGeneratedKeystrue是把数据库自动生成的主键回填到实体对象的 id 属性上后面关联插入日志时要用到。Service 层调用时要区分“短码不存在”“已过期”“已禁用”三种情况。由于 selectByCode 已经带了status 1查询结果为空时统一返回 404 即可。如果你需要区分原因给前端提示可以在查询里去掉 status 条件然后在 Service 里判断 status 字段。列表页的分页查询也是后端常见的写法select idselectPage resultTypecom.shortlink.entity.ShortLink SELECT id, short_code, long_url, expire_time, status, create_time FROM t_short_link ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectLIMIT 的 offset 由前端页码换算offset (page - 1) * pageSize。这个换算逻辑放在 Service 层前端传 page 和 pageSize 就行不要直接把 offset 暴露给前端否则分页参数能被人为改乱。4.2 跳转接口为什么我用 302 而不是 301短链接跳转接口是整个系统最简单的接口却也最容易踩坑核心就在一个状态码上。看下面这段 ControllerRestController RequestMapping(/api) public class RedirectController { private final ShortLinkService shortLinkService; public RedirectController(ShortLinkService shortLinkService) { this.shortLinkService shortLinkService; } GetMapping(/{shortCode}) public void redirect(PathVariable String shortCode, HttpServletResponse response) { ShortLink link shortLinkService.getLinkByCode(shortCode); if (link null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // 302 临时重定向每次都会经过本服务 response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); response.setHeader(Location, link.getLongUrl()); response.setHeader(Cache-Control, no-store); } }为什么用 302 而不是 301301 是永久重定向浏览器会缓存这个跳转结果同一个短码第二次访问时浏览器直接跳目标地址根本不请求你的服务器点击统计会漏短链接后期改了目标 URL老用户也会一直走缓存。302 每次请求都会先到后端再带Location头让浏览器跳转这样能记录点击、判断短码是否被禁用、也能随时改目标地址。大厂短链接对外跳转大多用 302。如果你明确知道这个短码永不修改也不做统计用 301 能省服务器带宽但这是一个“功能”与“性能”的取舍默认选 302 更安全。接口本身不返回 JSON 体而是设置响应状态码和 Location 头所以 Controller 方法返回 void。浏览器拿到 302 后会自动跟着 Location 走。这里补一个细节响应头加Cache-Control: no-store防止某些代理服务器缓存 302 响应导致短码修改后不生效。这个头在很多反代和浏览器实验性功能下是必要的。如果项目量上来跳转接口要加缓存。我一般先查 Redis命中就直接拿长链接不命中去查 MySQL再写回缓存。没有 Redis 的情况下直接用短链接主表查询也够用小项目不必强行上缓存public ShortLink getLinkByCode(String shortCode) { // 缓存 key避免和业务参数混淆 String cacheKey shortlink:url: shortCode; Object cachedUrl redisTemplate.opsForValue().get(cacheKey); if (cachedUrl ! null) { ShortLink link new ShortLink(); link.setShortCode(shortCode); link.setLongUrl(cachedUrl.toString()); return link; } ShortLink link shortLinkMapper.selectByCode(shortCode); if (link ! null) { // 24 小时过期失效后自动回源 redisTemplate.opsForValue().set(cacheKey, link.getLongUrl(), 24, TimeUnit.HOURS); } return link; }注意缓存里只存长链接字符串不存整个对象省序列化开销TTL 设 24 小时避免永久占内存。禁用短码时要主动删掉这个 key否则用户还能通过缓存跳转具体做法是在禁用接口里调用redisTemplate.delete(cacheKey)。如果项目里这一步没做就会出现“后台禁用了短链接线上还能访问”的诡异问题。4.3 Vue 前端路由、axios 封装与图表统计前端部分做的是管理界面不是跳转页。跳转是由后端 302 完成的Vue 里只需要一个后台页面来创建短码、看列表、看统计。路由配置比较简单import Vue from vue import VueRouter from vue-router import ShortLinkList from ../views/ShortLinkList.vue import CreateShortLink from ../views/CreateShortLink.vue Vue.use(VueRouter) const routes [ { path: /, name: ShortLinkList, component: ShortLinkList }, { path: /create, name: CreateShortLink, component: CreateShortLink } ]axios 封装一般是前端项目的标配。我习惯在 src/api 下建一个 request.js把 baseURL、超时时间和响应拦截统一处理import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL || /api, timeout: 10000 }) // 响应拦截直接解出 data 部分 service.interceptors.response.use( response response.data, error { if (error.response error.response.status 404) { console.warn(接口不存在或资源已失效) } return Promise.reject(error) } ) export default servicebaseURL在开发环境下走/api由 dev server 代理到后端生产环境也走/api由 Nginx 反向代理。这样前端代码不需要区分环境。timeout: 10000是 10 秒超时创建短链接时如果后端卡住前端能及时报错而不是一直转圈。创建短链接的页面逻辑可以很薄把请求交给 serviceexport default { data() { return { longUrl: , expireDays: 30 } }, methods: { async createLink() { const data await service.post(/shortlink, { longUrl: this.longUrl, expireDays: this.expireDays }) // 创建成功之后刷新列表 this.$emit(created, data) } } }这边要留意的参数是 expireDays前端传的是“多少天后过期”后端要换算成具体的过期时间点存库不要直接存天数。换算逻辑放在后端 Service 层这样即使以后加小程序端规则也是一份。统计图表是短链接系统的加分项。后端给一个按天聚合的接口SQL 长这样SELECT DATE(create_time) AS day, COUNT(*) AS visit_count FROM t_visit_log WHERE short_code #{shortCode} AND create_time #{startTime} GROUP BY DATE(create_time) ORDER BY day前端拿到这种二维数组直接喂给 ECharts 的 line 图。访问日志在跳转接口里异步写入注意不要在主流程里写日志否则日志表插入失败会影响跳转。常见做法是日志记录放到事务外或者用消息队列削峰项目里用 Spring 的Async就能解决问题。我在前面 2.3 小节已经写了 Async 的代码启动类记得加 EnableAsync否则日志写入会悄悄变回同步把跳转接口拖慢。5. 避坑与排查短链接项目最常翻车的 5 个地方5.1 联调与部署阶段的三个坑坑一前端请求接口报跨域浏览器直接拦截。现象Vue 页面打开后console 里报Access-Control-Allow-Origin相关错误接口在 Postman 里能通浏览器里不通。原因前端开发服务器跑在 3000 端口后端在 8080 端口协议、域名、端口三者里端口不同就构成跨域。Postman 没有同源策略所以测不出来。解决开发环境用 vue.config.js 的 proxy 转发请求变成同源后端再加一层 CORS 配置兜底。我一般这样写后端配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:3000) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }allowedOrigins 只放行本机开发地址生产环境走 Nginx 同域反代不需要 CORS。maxAge 是预检请求的缓存时间3600 秒可以避免每个请求都先 OPTIONS 一次。坑二部署后页面能打开但列表接口返回 404。现象前端构建产物丢到 Nginx访问首页正常点击任何一个按钮接口全部 404。原因前端构建后请求的是/api/shortlink但 Nginx 配置里没有把/api转给后端端口这个路径在静态目录里当然找不到。解决在 Nginx 里加反向代理location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意proxy_pass结尾的斜杠有没有写写法不同路径拼接规则也不同。不带斜杠是保留完整路径带斜杠会替换匹配部分后端接口路径要与之一致。坑三刷新页面 404F5 一次挂一次。现象列表页面/能打开但/create或详情页/detail/xxx刷新后Nginx 直接返回 404。原因Vue Router 用了 history 模式路由是前端模拟的服务器上没有对应的物理文件。解决Nginx 的 location / 里加 try_files让所有路径都回退到 index.htmllocation / { try_files $uri $uri/ /index.html; }这段配置是 history 模式的后悔药项目里最容易漏的就是这一行。很多教程只给到“npm run build 后丢进 Nginx”没说刷新问题等演示时一按 F5 就翻车。5.2 跳转与数据层的坑坑四短码偶尔跳到别人的链接。现象用户反馈 A 的短码打开的是 B 的页面不是每次都有间隔一段时间出现一次。原因短码生成时先SELECT查有没有重复没有就INSERT两个请求并发时都查到没有就插入了相同短码。发号器本身不会重复但手写逻辑可能绕过发号器直接随机制造短码或者代码里用了随机数方案。解决short_code 字段必须加唯一索引插入时捕获 DuplicateKeyException 重试。查重只能算第一道防线唯一索引才是兜底。注意在压测时这个坑最容易暴露并发一高问题就出来了。坑五带 emoji 的长链接存不进 MySQL。现象创建短码时接口报Incorrect string value: \xF0\x9F...普通链接正常。原因数据库或表字符集是 utf8而 utf8 在 MySQL 里最多只能存 3 字节emoji 是 4 字节。连接串里如果只写 characterEncodingutf8 也救不回来。解决建库建表用 utf8mb4连接串保留characterEncodingutf8这是 MySQL 驱动的老写法它会把 utf8 映射成 utf8mb4。MySQL 8 的驱动默认连接字符集已经没问题但库里表里必须是 utf8mb4。我在连接池参数里会加一行useUnicodetrue配合前面的 characterEncoding 一起用。这个坑属于典型的一次配置错、处处受影响建库脚本开头没写 utf8mb4后续所有表都会继承错误字符集。6. 上线前的验证压测、防刷与最后的回归检查短链接项目能不能上线不是功能跑通了就算数。我一般会做三件事压测跳转接口、验证防刷策略、回归核心链路。压测最简单的方式是用 Apache ab 打跳转接口ab -n 10000 -c 100 http://localhost:8080/api/aBc123参数说明-n是总请求数 10000-c是并发数 100。观察两个数字Requests per second 和 Time per request。如果带着 Redis 缓存单机 Spring Boot 跳到几千 QPS 是很正常的如果没加缓存压测时数据库连接数和慢 SQL 会立刻暴露出来。我第一次压这个项目时就是没加缓存MySQL 的 CPU 直接跑满连接池报Connection is not available。后来加上缓存同样压测下数据库几乎不干活。从那以后我每次接手类似服务第一件事都是先确认缓存逻辑有没有加。防刷策略也要验证。短链接最怕的不是跳转而是被人批量生成用来做推广或者钓鱼。我在项目里加了一个简单的 IP 维度过滤器同一个 IP 每分钟生成短码超过 20 次直接拒绝。实现不复杂用 Spring 拦截器加一个ConcurrentHashMap做计数器定时清空也可以单机够用真要上生产再换成 Redis 计数// 伪代码表示限流逻辑 if (rateLimiter.tryAcquire(ip, create, 60, 20)) { // 允许创建 } else { // 返回 429 或提示操作频繁 }最后回归核心链路我习惯按这个顺序走一遍创建短码、访问短码跳转、查看访问统计、禁用短码、再访问短码。创建后马上跳转确认 302 生效把长链接改掉确认第二次访问走的是新地址禁用后再次访问确认返回 404。这套源码我已经按上面的结构重新整理过目录数据库脚本也在 sql 目录里下载后按第 2 章的顺序启动就行。跑一遍比我上面说的所有坑都管用希望这些验证步骤能帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →