设计一个短连接系统:Java实习生面试实践
最近我面试了一个 Java 实习生岗位,面试官提出了一个有趣的问题:设计一个短连接系统。这类问题在实际开发中很常见,比如我们经常在社交媒体或营销活动中看到类似 t.cn/abc123
的短链接,它们背后是如何实现的呢?下面我将分享我的思考过程,并基于 MyBatis 和 Spring Boot 提供一个简单的代码实践。
需求分析
短连接系统的核心功能是将一个长 URL(比如 https://www.example.com/very/long/url/with/parameters
)转换为一个短链接(比如 http://short.url/abc123
),用户访问短链接时能重定向到原始长 URL。主要需求包括:
- 生成短链接:输入长 URL,返回一个唯一的短链接。
- 重定向:通过短链接访问时,跳转到对应的长 URL。
- 持久化:短链接和长 URL 的映射关系需要存储到数据库。
- 唯一性:短链接必须唯一,避免冲突。
- 扩展性:系统要支持高并发和大规模 URL 转换。
设计思路
短链接生成算法
短链接通常是 6-8 位字符组成,可以用字母和数字(62 进制:0-9, a-z, A-Z)。我选择以下方案:
- 使用自增 ID(如数据库主键)生成唯一标识。
- 将 ID 转换为 62 进制字符串,确保短链接简洁且唯一。
- 例如:ID
12345
转为 62 进制可能是3D7
。
系统架构
- 前端:用户输入长 URL,获取短链接;访问短链接时跳转。
- 后端:Spring Boot 提供 RESTful API,MyBatis 操作数据库。
- 数据库:存储长 URL 和短链接的映射。
- 流程 :
- 用户提交长 URL,后端生成短链接并存入数据库。
- 用户访问短链接,后端查询数据库并返回 302 重定向。
技术选型
- Spring Boot:快速搭建 RESTful 服务。
- MyBatis:轻量级 ORM,操作数据库。
- MySQL:存储 URL 映射关系。
代码实践
以下是基于 MyBatis 和 Spring Boot 的实现。
1. 数据库表设计
sql
CREATE TABLE short_url (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
long_url VARCHAR(512) NOT NULL,
short_code VARCHAR(8) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
2. 项目结构
css
short-url-service
├── src
│ ├── main
│ │ ├── java/com/example/shorturl
│ │ │ ├── controller/ShortUrlController.java
│ │ │ ├── service/ShortUrlService.java
│ │ │ ├── mapper/ShortUrlMapper.java
│ │ │ ├── entity/ShortUrl.java
│ │ │ └── ShortUrlApplication.java
│ │ └── resources
│ │ ├── application.yml
│ │ └── mapper/ShortUrlMapper.xml
3. 实体类
java
package com.example.shorturl.entity;
public class ShortUrl {
private Long id;
private String longUrl;
private String shortCode;
private String createdAt;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getLongUrl() { return longUrl; }
public void setLongUrl(String longUrl) { this.longUrl = longUrl; }
public String getShortCode() { return shortCode; }
public void setShortCode(String shortCode) { this.shortCode = shortCode; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
}
4. Mapper 接口
java
package com.example.shorturl.mapper;
import com.example.shorturl.entity.ShortUrl;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ShortUrlMapper {
void insert(ShortUrl shortUrl);
ShortUrl findByShortCode(String shortCode);
}
5. MyBatis XML
文件:resources/mapper/ShortUrlMapper.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.shorturl.mapper.ShortUrlMapper">
<insert id="insert" parameterType="com.example.shorturl.entity.ShortUrl" useGeneratedKeys="true" keyProperty="id">
INSERT INTO short_url (long_url, short_code) VALUES (#{longUrl}, #{shortCode})
</insert>
<select id="findByShortCode" parameterType="string" resultType="com.example.shorturl.entity.ShortUrl">
SELECT * FROM short_url WHERE short_code = #{shortCode}
</select>
</mapper>
6. 服务层
java
package com.example.shorturl.service;
import com.example.shorturl.entity.ShortUrl;
import com.example.shorturl.mapper.ShortUrlMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ShortUrlService {
private static final String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
@Autowired
private ShortUrlMapper shortUrlMapper;
public String createShortUrl(String longUrl) {
ShortUrl shortUrl = new ShortUrl();
shortUrl.setLongUrl(longUrl);
// 插入数据库,获取自增 ID
shortUrlMapper.insert(shortUrl);
String shortCode = toBase62(shortUrl.getId());
shortUrl.setShortCode(shortCode);
// 更新 shortCode
shortUrlMapper.insert(shortUrl); // 这里应为 update,简化起见直接重插
return "http://short.url/" + shortCode;
}
public String getLongUrl(String shortCode) {
ShortUrl shortUrl = shortUrlMapper.findByShortCode(shortCode);
return shortUrl != null ? shortUrl.getLongUrl() : null;
}
private String toBase62(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(BASE62.charAt((int) (num % 62)));
num /= 62;
}
return sb.reverse().toString();
}
}
7. 控制器
java
package com.example.shorturl.controller;
import com.example.shorturl.service.ShortUrlService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@RequestMapping("/api")
public class ShortUrlController {
@Autowired
private ShortUrlService shortUrlService;
@PostMapping("/shorten")
public ResponseEntity<String> shortenUrl(@RequestBody String longUrl) {
String shortUrl = shortUrlService.createShortUrl(longUrl);
return new ResponseEntity<>(shortUrl, HttpStatus.OK);
}
@GetMapping("/{shortCode}")
public void redirect(@PathVariable String shortCode, HttpServletResponse response) throws IOException {
String longUrl = shortUrlService.getLongUrl(shortCode);
if (longUrl != null) {
response.sendRedirect(longUrl);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
8. 配置文件
文件:resources/application.yml
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/short_url_db
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
9. 主应用
java
package com.example.shorturl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShortUrlApplication {
public static void main(String[] args) {
SpringApplication.run(ShortUrlApplication.class, args);
}
}
测试
- 启动 Spring Boot 应用。
- POST 请求
http://localhost:8080/api/shorten
,Body 为{"longUrl": "https://www.example.com"}
,返回短链接。 - 访问
http://localhost:8080/api/abc123
,跳转到原始 URL。
优化方向
- 缓存:用 Redis 缓存短链接映射,提升查询性能。
- 分布式 ID:高并发下用雪花算法替代自增 ID。
- 安全性:校验长 URL 合法性,防止恶意输入。
总结
通过这个实践,我学会了如何从需求分析到代码实现设计一个短连接系统。面试官可能会关注我的思路清晰度和代码规范性。这个方案虽然简单,但涵盖了核心功能,是个不错的起点。希望这篇博客对你也有帮助!