侧边栏壁纸
博主头像
小黄的日记

行动起来,活在当下

  • 累计撰写 19 篇文章
  • 累计创建 24 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

如何设计一个短链系统(Java实现)

henry
2025-10-24 / 0 评论 / 0 点赞 / 9 阅读 / 0 字

如何设计一个短链系统(Java实现)

前言

短链系统是现代互联网应用中常见的功能,它能将长URL转换为短URL,便于分享和传播。本文将详细介绍如何设计和实现一个完整的短链系统。

系统架构设计

核心功能

  1. URL缩短:将长URL转换为短URL
  2. URL重定向:通过短URL访问原始URL
  3. 统计分析:记录访问次数、来源等信息
  4. 过期管理:支持设置URL过期时间

技术选型

  • 后端框架:Spring Boot
  • 数据库:MySQL + Redis
  • 缓存:Redis
  • 消息队列:RabbitMQ(可选)

数据库设计

短链表(short_url)

CREATE TABLE short_url (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    short_code VARCHAR(10) NOT NULL UNIQUE COMMENT '短链码',
    original_url TEXT NOT NULL COMMENT '原始URL',
    user_id BIGINT COMMENT '用户ID',
    expire_time DATETIME COMMENT '过期时间',
    click_count INT DEFAULT 0 COMMENT '点击次数',
    status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_short_code (short_code),
    INDEX idx_user_id (user_id),
    INDEX idx_create_time (create_time)
);

访问记录表(access_log)

CREATE TABLE access_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    short_code VARCHAR(10) NOT NULL,
    ip_address VARCHAR(45),
    user_agent TEXT,
    referer VARCHAR(500),
    access_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_short_code (short_code),
    INDEX idx_access_time (access_time)
);

核心算法实现

短链码生成算法

方案一:Base62编码

@Component
public class Base62Encoder {

    private static final String BASE62_CHARS = 
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    private static final int BASE = BASE62_CHARS.length();

    /**
     * 将数字ID转换为Base62字符串
     */
    public String encode(long id) {
        if (id == 0) return "0";

        StringBuilder sb = new StringBuilder();
        while (id > 0) {
            sb.append(BASE62_CHARS.charAt((int)(id % BASE)));
            id /= BASE;
        }
        return sb.reverse().toString();
    }

    /**
     * 将Base62字符串转换为数字ID
     */
    public long decode(String shortCode) {
        long result = 0;
        long power = 1;

        for (int i = shortCode.length() - 1; i >= 0; i--) {
            char c = shortCode.charAt(i);
            int index = BASE62_CHARS.indexOf(c);
            if (index == -1) {
                throw new IllegalArgumentException("Invalid character: " + c);
            }
            result += index * power;
            power *= BASE;
        }
        return result;
    }
}

方案二:雪花算法 + Base62

@Component
public class SnowflakeIdGenerator {

    private final long workerId;
    private final long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    // 各部分位数
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long sequenceBits = 12L;

    // 最大值
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    // 位移
    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // 起始时间戳 (2021-01-01)
    private final long twepoch = 1609459200000L;

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("Worker ID can't be greater than " + maxWorkerId + " or less than 0");
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("Datacenter ID can't be greater than " + maxDatacenterId + " or less than 0");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate id");
        }

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) |
               (datacenterId << datacenterIdShift) |
               (workerId << workerIdShift) |
               sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}

核心业务实现

实体类定义

@Entity
@Table(name = "short_url")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ShortUrl {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "short_code", unique = true, nullable = false, length = 10)
    private String shortCode;

    @Column(name = "original_url", nullable = false, columnDefinition = "TEXT")
    private String originalUrl;

    @Column(name = "user_id")
    private Long userId;

    @Column(name = "expire_time")
    private LocalDateTime expireTime;

    @Column(name = "click_count")
    private Integer clickCount = 0;

    @Column(name = "status")
    private Integer status = 1;

    @Column(name = "create_time")
    private LocalDateTime createTime;

    @Column(name = "update_time")
    private LocalDateTime updateTime;

    @PrePersist
    protected void onCreate() {
        createTime = LocalDateTime.now();
        updateTime = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updateTime = LocalDateTime.now();
    }
}

Repository层

@Repository
public interface ShortUrlRepository extends JpaRepository<ShortUrl, Long> {

    Optional<ShortUrl> findByShortCodeAndStatus(String shortCode, Integer status);

    Optional<ShortUrl> findByOriginalUrlAndUserId(String originalUrl, Long userId);

    List<ShortUrl> findByUserIdAndStatus(Long userId, Integer status, Pageable pageable);

    @Modifying
    @Query("UPDATE ShortUrl s SET s.clickCount = s.clickCount + 1 WHERE s.shortCode = :shortCode")
    int incrementClickCount(@Param("shortCode") String shortCode);
}

服务层实现

@Service
@Transactional
@Slf4j
public class ShortUrlService {

    @Autowired
    private ShortUrlRepository shortUrlRepository;

    @Autowired
    private Base62Encoder base62Encoder;

    @Autowired
    private SnowflakeIdGenerator snowflakeIdGenerator;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String CACHE_PREFIX = "short_url:";
    private static final int CACHE_EXPIRE_HOURS = 24;

    /**
     * 创建短链
     */
    public String createShortUrl(String originalUrl, Long userId, LocalDateTime expireTime) {
        // 参数验证
        if (!isValidUrl(originalUrl)) {
            throw new IllegalArgumentException("Invalid URL format");
        }

        // 检查是否已存在
        Optional<ShortUrl> existing = shortUrlRepository.findByOriginalUrlAndUserId(originalUrl, userId);
        if (existing.isPresent() && existing.get().getStatus() == 1) {
            return existing.get().getShortCode();
        }

        // 生成短链码
        String shortCode = generateShortCode();

        // 保存到数据库
        ShortUrl shortUrl = new ShortUrl();
        shortUrl.setShortCode(shortCode);
        shortUrl.setOriginalUrl(originalUrl);
        shortUrl.setUserId(userId);
        shortUrl.setExpireTime(expireTime);

        shortUrlRepository.save(shortUrl);

        // 缓存到Redis
        cacheShortUrl(shortCode, originalUrl);

        log.info("Created short URL: {} -> {}", shortCode, originalUrl);
        return shortCode;
    }

    /**
     * 获取原始URL
     */
    public String getOriginalUrl(String shortCode) {
        // 先从缓存获取
        String cachedUrl = getCachedUrl(shortCode);
        if (cachedUrl != null) {
            return cachedUrl;
        }

        // 从数据库获取
        Optional<ShortUrl> shortUrl = shortUrlRepository.findByShortCodeAndStatus(shortCode, 1);
        if (!shortUrl.isPresent()) {
            throw new RuntimeException("Short URL not found: " + shortCode);
        }

        ShortUrl url = shortUrl.get();

        // 检查是否过期
        if (url.getExpireTime() != null && url.getExpireTime().isBefore(LocalDateTime.now())) {
            throw new RuntimeException("Short URL expired: " + shortCode);
        }

        // 异步更新点击次数
        updateClickCountAsync(shortCode);

        // 缓存结果
        cacheShortUrl(shortCode, url.getOriginalUrl());

        return url.getOriginalUrl();
    }

    /**
     * 生成短链码
     */
    private String generateShortCode() {
        // 使用雪花算法生成ID,然后转换为Base62
        long id = snowflakeIdGenerator.nextId();
        return base62Encoder.encode(id);
    }

    /**
     * URL格式验证
     */
    private boolean isValidUrl(String url) {
        try {
            new URL(url);
            return url.startsWith("http://") || url.startsWith("https://");
        } catch (MalformedURLException e) {
            return false;
        }
    }

    /**
     * 缓存短链
     */
    private void cacheShortUrl(String shortCode, String originalUrl) {
        try {
            redisTemplate.opsForValue().set(
                CACHE_PREFIX + shortCode, 
                originalUrl, 
                CACHE_EXPIRE_HOURS, 
                TimeUnit.HOURS
            );
        } catch (Exception e) {
            log.warn("Failed to cache short URL: {}", shortCode, e);
        }
    }

    /**
     * 获取缓存的URL
     */
    private String getCachedUrl(String shortCode) {
        try {
            return (String) redisTemplate.opsForValue().get(CACHE_PREFIX + shortCode);
        } catch (Exception e) {
            log.warn("Failed to get cached URL: {}", shortCode, e);
            return null;
        }
    }

    /**
     * 异步更新点击次数
     */
    @Async
    private void updateClickCountAsync(String shortCode) {
        try {
            shortUrlRepository.incrementClickCount(shortCode);
        } catch (Exception e) {
            log.error("Failed to update click count for: {}", shortCode, e);
        }
    }
}

控制器实现

@RestController
@RequestMapping("/api/short-url")
@Slf4j
public class ShortUrlController {

    @Autowired
    private ShortUrlService shortUrlService;

    /**
     * 创建短链
     */
    @PostMapping("/create")
    public ResponseEntity<ApiResponse<String>> createShortUrl(@RequestBody CreateShortUrlRequest request) {
        try {
            String shortCode = shortUrlService.createShortUrl(
                request.getOriginalUrl(),
                request.getUserId(),
                request.getExpireTime()
            );

            String shortUrl = "https://short.ly/" + shortCode;
            return ResponseEntity.ok(ApiResponse.success(shortUrl));

        } catch (Exception e) {
            log.error("Failed to create short URL", e);
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("Failed to create short URL: " + e.getMessage()));
        }
    }

    /**
     * 短链重定向
     */
    @GetMapping("/{shortCode}")
    public ResponseEntity<Void> redirect(@PathVariable String shortCode, HttpServletRequest request) {
        try {
            String originalUrl = shortUrlService.getOriginalUrl(shortCode);

            // 记录访问日志
            logAccess(shortCode, request);

            return ResponseEntity.status(HttpStatus.FOUND)
                .location(URI.create(originalUrl))
                .build();

        } catch (Exception e) {
            log.error("Failed to redirect short URL: {}", shortCode, e);
            return ResponseEntity.notFound().build();
        }
    }

    /**
     * 获取短链信息
     */
    @GetMapping("/info/{shortCode}")
    public ResponseEntity<ApiResponse<ShortUrlInfo>> getShortUrlInfo(@PathVariable String shortCode) {
        try {
            // 实现获取短链详细信息的逻辑
            return ResponseEntity.ok(ApiResponse.success(null));
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("Failed to get short URL info: " + e.getMessage()));
        }
    }

    private void logAccess(String shortCode, HttpServletRequest request) {
        // 异步记录访问日志
        // 可以使用消息队列或异步方法
    }
}

性能优化策略

1. 缓存策略

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager.Builder builder = RedisCacheManager
            .RedisCacheManagerBuilder
            .fromConnectionFactory(redisConnectionFactory())
            .cacheDefaults(cacheConfiguration());

        return builder.build();
    }

    private RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(24))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

2. 数据库优化

-- 分表策略(按时间分表)
CREATE TABLE short_url_202501 LIKE short_url;
CREATE TABLE short_url_202502 LIKE short_url;

-- 索引优化
CREATE INDEX idx_short_code_status ON short_url(short_code, status);
CREATE INDEX idx_user_create_time ON short_url(user_id, create_time);

-- 读写分离配置

3. 限流策略

@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String RATE_LIMIT_KEY = "rate_limit:";
    private static final int MAX_REQUESTS = 100; // 每分钟最大请求数

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String clientIp = getClientIp(request);
        String key = RATE_LIMIT_KEY + clientIp;

        String count = redisTemplate.opsForValue().get(key);
        if (count == null) {
            redisTemplate.opsForValue().set(key, "1", 60, TimeUnit.SECONDS);
        } else if (Integer.parseInt(count) >= MAX_REQUESTS) {
            response.setStatus(429); // Too Many Requests
            return false;
        } else {
            redisTemplate.opsForValue().increment(key);
        }

        return true;
    }

    private String getClientIp(HttpServletRequest request) {
        // 获取真实IP的逻辑
        return request.getRemoteAddr();
    }
}

分布式部署方案

1. 微服务架构

# docker-compose.yml
version: '3.8'
services:
  short-url-service:
    image: short-url-service:latest
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - MYSQL_HOST=mysql
      - REDIS_HOST=redis
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: short_url
    volumes:
      - mysql_data:/var/lib/mysql

  redis:
    image: redis:6.2
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - short-url-service

volumes:
  mysql_data:
  redis_data:

2. 负载均衡配置

upstream short_url_backend {
    server short-url-service-1:8080;
    server short-url-service-2:8080;
    server short-url-service-3:8080;
}

server {
    listen 80;
    server_name short.ly;

    location / {
        proxy_pass http://short_url_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

监控与运维

1. 健康检查

@RestController
public class HealthController {

    @Autowired
    private ShortUrlRepository repository;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/health")
    public ResponseEntity<Map<String, String>> health() {
        Map<String, String> status = new HashMap<>();

        // 检查数据库连接
        try {
            repository.count();
            status.put("database", "UP");
        } catch (Exception e) {
            status.put("database", "DOWN");
        }

        // 检查Redis连接
        try {
            redisTemplate.opsForValue().get("health_check");
            status.put("redis", "UP");
        } catch (Exception e) {
            status.put("redis", "DOWN");
        }

        status.put("service", "UP");
        return ResponseEntity.ok(status);
    }
}

2. 指标监控

@Component
public class MetricsCollector {

    private final MeterRegistry meterRegistry;
    private final Counter shortUrlCreatedCounter;
    private final Counter shortUrlAccessCounter;
    private final Timer redirectTimer;

    public MetricsCollector(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.shortUrlCreatedCounter = Counter.builder("short_url_created_total")
            .description("Total number of short URLs created")
            .register(meterRegistry);
        this.shortUrlAccessCounter = Counter.builder("short_url_access_total")
            .description("Total number of short URL accesses")
            .register(meterRegistry);
        this.redirectTimer = Timer.builder("short_url_redirect_duration")
            .description("Time taken to redirect short URL")
            .register(meterRegistry);
    }

    public void incrementCreatedCounter() {
        shortUrlCreatedCounter.increment();
    }

    public void incrementAccessCounter() {
        shortUrlAccessCounter.increment();
    }

    public Timer.Sample startRedirectTimer() {
        return Timer.start(meterRegistry);
    }
}

安全考虑

1. 防止恶意URL

@Component
public class UrlSecurityChecker {

    private static final Set<String> BLOCKED_DOMAINS = Set.of(
        "malicious-site.com",
        "phishing-site.com"
    );

    private static final Pattern SUSPICIOUS_PATTERN = Pattern.compile(
        ".*(exec|script|javascript|vbscript).*", 
        Pattern.CASE_INSENSITIVE
    );

    public boolean isUrlSafe(String url) {
        try {
            URL urlObj = new URL(url);
            String host = urlObj.getHost().toLowerCase();

            // 检查黑名单域名
            if (BLOCKED_DOMAINS.contains(host)) {
                return false;
            }

            // 检查可疑模式
            if (SUSPICIOUS_PATTERN.matcher(url).matches()) {
                return false;
            }

            return true;
        } catch (MalformedURLException e) {
            return false;
        }
    }
}

2. 访问控制

@Component
public class AccessControlService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String BLACKLIST_KEY = "ip_blacklist:";
    private static final String SUSPICIOUS_KEY = "suspicious_ip:";

    public boolean isIpBlocked(String ip) {
        return redisTemplate.hasKey(BLACKLIST_KEY + ip);
    }

    public void blockIp(String ip, Duration duration) {
        redisTemplate.opsForValue().set(
            BLACKLIST_KEY + ip, 
            "blocked", 
            duration.toSeconds(), 
            TimeUnit.SECONDS
        );
    }

    public void markSuspiciousActivity(String ip) {
        String key = SUSPICIOUS_KEY + ip;
        String count = redisTemplate.opsForValue().get(key);

        if (count == null) {
            redisTemplate.opsForValue().set(key, "1", 3600, TimeUnit.SECONDS);
        } else {
            int suspiciousCount = Integer.parseInt(count);
            if (suspiciousCount >= 10) {
                blockIp(ip, Duration.ofHours(24));
            } else {
                redisTemplate.opsForValue().increment(key);
            }
        }
    }
}

总结

本文详细介绍了短链系统的设计与实现,涵盖了以下关键技术点:

  1. 系统架构设计:采用微服务架构,支持水平扩展
  2. 核心算法:Base62编码 + 雪花算法,保证短链的唯一性和可读性
  3. 性能优化:多级缓存、数据库优化、限流策略
  4. 分布式部署:Docker容器化、负载均衡、服务发现
  5. 监控运维:健康检查、指标监控、日志收集
  6. 安全防护:URL安全检查、访问控制、防刷机制

通过这套完整的解决方案,可以构建一个高性能、高可用、安全可靠的短链系统,满足生产环境的各种需求。

关键技术要点总结:

  • 使用雪花算法保证分布式环境下ID的唯一性
  • Base62编码生成用户友好的短链码
  • Redis缓存提升访问性能
  • 异步处理提高系统吞吐量
  • 完善的监控和安全机制

这个系统设计不仅适用于短链服务,其中的很多设计思想和技术方案也可以应用到其他高并发系统的开发中。

0

评论区