短链接系统设计概要

文摘   2024-07-18 08:24   福建  

设计一个短链接系统,从功能需求、系统架构、安全性、扩展性等多方面进行深入思考,可以分为以下几个关键部分:

功能需求

  1. 长网址生成短链接

  2. 短链接重定向到原始长网址

  3. 短链接有效期管理

  4. 点击统计分析

  5. 管理后台(可选)

设计一个短链接系统的整体架构

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 (如果你打算支持短链接的有效期)

短链接生成逻辑
  1. 接收长链接请求。

  2. 生成唯一ID(自增ID或UUID)。

  3. 使用Base62编码将ID转为短链接。

  4. 将短链接和长链接保存到数据库。

短链接跳转逻辑
  1. 接收短链接请求。

  2. 查询数据库获取对应的长链接。

  3. 返回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());
}
);
}
}


程序员技术成长之路
技术简介:涉及多个编程语言go、php、java。曾任职于互联网大厂,有多年开发编程经验。欢迎互相交流