设计一个短链接系统,从功能需求、系统架构、安全性、扩展性等多方面进行深入思考,可以分为以下几个关键部分:
功能需求
长网址生成短链接
短链接重定向到原始长网址
短链接有效期管理
点击统计分析
管理后台(可选)
设计一个短链接系统的整体架构
1. 短链接生成策略
6-10位的短链接:可以由62个字符(a-z, A-Z, 0-9)组成,这样的短链接空间很大,可以避免冲突。
唯一ID和Base62编码:将唯一ID转换为62进制编码来生成短链接。
2. 数据存储
数据库(如MySQL、PostgreSQL):存储短链接和长链接的映射关系。
缓存(如Redis):加速读取短链接映射。
3. 服务架构
短链接生成服务:负责接收长链接并生成短链接。
短链接跳转服务:接收短链接请求并跳转到长链接。
统计服务:收集并分析点击数据。
4. 主要组件设计
数据库设计(表结构示例)
UrlMapping表
id: 自增唯一ID
short_url: VARCHAR
long_url: TEXT
created_at: TIMESTAMP
expiration_date: TIMESTAMP (如果你打算支持短链接的有效期)
短链接生成逻辑
接收长链接请求。
生成唯一ID(自增ID或UUID)。
使用Base62编码将ID转为短链接。
将短链接和长链接保存到数据库。
短链接跳转逻辑
接收短链接请求。
查询数据库获取对应的长链接。
返回HTTP 301重定向到长链接。
数据库设计
数据库设计的关键是在 UrlMapping
表中存储短链接和长链接的映射关系,并且可以扩展以支持点击统计、有效期管理等功能。
UrlMapping
表
这个表主要用来存储短链接和对应的长链接,以及生成时间和过期时间等信息。
CREATE TABLE UrlMapping (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
short_url VARCHAR(255) NOT NULL UNIQUE,
long_url TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expiration_date TIMESTAMP DEFAULT NULL
);sql复制代码
id
: 一个自增的唯一ID,用于生成短链接。short_url
: 短链接字符串。long_url
: 对应的长链接。created_at
: 短链接的生成时间。expiration_date
: 短链接的过期时间(可选)。
可能的扩展表
点击统计表 UrlClickStats
如果需要实现点击统计功能,可以创建一个专门的表来记录短链接的点击统计信息。
CREATE TABLE UrlClickStats (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
short_url VARCHAR(255) NOT NULL,
click_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_ip VARCHAR(45) DEFAULT NULL,
user_agent TEXT DEFAULT NULL,
referrer TEXT DEFAULT NULL,
INDEX(short_url, click_time)
);sql复制代码
id
: 唯一ID。short_url
: 记录点击的短链接。click_time
: 点击发生的时间。user_ip
: 用户IP地址(可选,用于分析)。user_agent
: 用户代理字符串(可选,用于分析)。referrer
: 引用链接(可选,用于分析)。
Spring Data JPA 实现
UrlMapping
实体类
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
public class UrlMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String shortUrl;
@Column(nullable = false, columnDefinition="TEXT")
private String longUrl;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = true)
private LocalDateTime expirationDate;
// Getters and Setters
}java复制代码
UrlClickStats
实体类
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
public class UrlClickStats {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String shortUrl;
@Column(nullable = false)
private LocalDateTime clickTime;
@Column(nullable = true)
private String userIp;
@Column(nullable = true, columnDefinition="TEXT")
private String userAgent;
@Column(nullable = true, columnDefinition="TEXT")
private String referrer;
// Getters and Setters
}java复制代码
仓库层
UrlMappingRepository
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UrlMappingRepository extends JpaRepository<UrlMapping, Long> {
Optional<UrlMapping> findByShortUrl(String shortUrl);
}java复制代码
UrlClickStatsRepository
import org.springframework.data.jpa.repository.JpaRepository;
public interface UrlClickStatsRepository extends JpaRepository<UrlClickStats, Long> {
}java复制代码
服务层
短链接服务类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
public class UrlMappingService {
@Autowired
private UrlMappingRepository urlMappingRepository;
@Autowired
private UrlClickStatsRepository urlClickStatsRepository;
private final static String BASE62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
public UrlMapping createShortUrl(String longUrl) {
UrlMapping urlMapping = new UrlMapping();
urlMapping.setLongUrl(longUrl);
urlMapping.setCreatedAt(LocalDateTime.now());
urlMapping = urlMappingRepository.save(urlMapping);
urlMapping.setShortUrl(generateShortUrl(urlMapping.getId()));
return urlMappingRepository.save(urlMapping);
}
public String generateShortUrl(Long id) {
StringBuilder shortUrl = new StringBuilder();
while (id > 0) {
shortUrl.append(BASE62.charAt((int)(id % 62)));
id /= 62;
}
return shortUrl.reverse().toString();
}
public Optional<String> getLongUrl(String shortUrl) {
return urlMappingRepository.findByShortUrl(shortUrl).map(UrlMapping::getLongUrl);
}
public void recordClick(String shortUrl, String userIp, String userAgent, String referrer) {
UrlClickStats clickStats = new UrlClickStats();
clickStats.setShortUrl(shortUrl);
clickStats.setClickTime(LocalDateTime.now());
clickStats.setUserIp(userIp);
clickStats.setUserAgent(userAgent);
clickStats.setReferrer(referrer);
urlClickStatsRepository.save(clickStats);
}
}java复制代码
控制器层
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@RequestMapping("/api")
public class UrlMappingController {
@Autowired
private UrlMappingService urlMappingService;
@PostMapping("/shorten")
public UrlMapping shortenUrl(@RequestBody String longUrl) {
return urlMappingService.createShortUrl(longUrl);
}
@GetMapping("/{shortUrl}")
public void redirect(@PathVariable String shortUrl, HttpServletRequest request, HttpServletResponse response) throws IOException {
urlMappingService.getLongUrl(shortUrl).ifPresentOrElse(
longUrl -> {
try {
// 记录点击信息
String userIp = request.getRemoteAddr();
String userAgent = request.getHeader("User-Agent");
String referrer = request.getHeader("Referer");
urlMappingService.recordClick(shortUrl, userIp, userAgent, referrer);
response.sendRedirect(longUrl);
} catch (IOException e) {
e.printStackTrace();
}
},
() -> {
response.setStatus(HttpStatus.NOT_FOUND.value());
}
);
}
}