设计一个短连接系统:Java实习生面试实践

20 阅读3分钟

设计一个短连接系统:Java实习生面试实践(修复版)

在面试 Java 实习生岗位时,面试官让我设计一个短连接系统。经过多次迭代,我修复了之前的问题,包括自增 ID 未正确返回的潜在风险。以下是修复后的内容,基于 MyBatis 和 Spring Boot 实现。

需求分析

短连接系统将长 URL 转换为短链接,用户访问短链接时重定向到原始 URL。主要需求:

  1. 生成短链接:输入长 URL,返回唯一短链接。
  2. 重定向:通过短链接跳转到长 URL。
  3. 持久化:映射关系存储到数据库。
  4. 唯一性:短链接必须唯一。
  5. 扩展性:支持高并发。

设计思路

  • 短链接生成:用数据库自增 ID 转为 62 进制字符串。
  • 流程:插入长 URL 后获取 ID,生成短链接并更新记录。
  • 技术栈:Spring Boot + MyBatis + MySQL。

代码实践

1. 数据库表设计

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
);

-- 设置自增起始值,避免短链接太短
ALTER TABLE short_url AUTO_INCREMENT = 10000;

说明
自增 ID 从 10000 开始,确保初始短链接长度合理(如 10000 转为 2Bi)。

2. 实体类

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; }
}

3. Mapper 接口

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);
    void update(ShortUrl shortUrl);
    ShortUrl findByShortCode(String shortCode);
}

4. MyBatis XML

文件:resources/mapper/ShortUrlMapper.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>
    <update id="update" parameterType="com.example.shorturl.entity.ShortUrl">
        UPDATE short_url SET short_code = #{shortCode} WHERE id = #{id}
    </update>
    <select id="findByShortCode" parameterType="string" resultType="com.example.shorturl.entity.ShortUrl">
        SELECT * FROM short_url WHERE short_code = #{shortCode}
    </select>
</mapper>

修复说明
insert 使用 useGeneratedKeys="true"keyProperty="id",确保数据库生成的自增 ID 被自动设置到 shortUrl 对象的 id 字段。

5. 服务层

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);
        shortUrl.setShortCode("temp"); // 临时值,避免 NOT NULL 约束
        
        // 插入记录,MyBatis 会将自增 ID 设置到 shortUrl.id
        shortUrlMapper.insert(shortUrl);
        
        // 检查 ID 是否正确返回
        if (shortUrl.getId() == null) {
            throw new RuntimeException("Failed to retrieve generated ID");
        }
        
        // 生成 shortCode 并更新
        String shortCode = toBase62(shortUrl.getId());
        shortUrl.setShortCode(shortCode);
        shortUrlMapper.update(shortUrl);
        
        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();
    }
}

修复说明

  • 添加了 if (shortUrl.getId() == null) 检查,确保 insert 后 ID 被正确返回。
  • 如果 ID 未返回,抛出异常,便于调试。这是对 MyBatis 配置的一种防御性编程。

6. 控制器

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);
        }
    }
}

7. 配置文件

文件:resources/application.yml

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

8. 主应用

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);
    }
}

测试

  1. 启动应用。
  2. POST 请求 http://localhost:8080/api/shorten,Body 为 {"longUrl": "https://www.example.com"},返回短链接。
  3. 访问 http://localhost:8080/api/2Bi,跳转到原始 URL。

优化方向

  • 缓存:用 Redis 提升查询性能。
  • 分布式 ID:用雪花算法支持高并发。
  • 短链接长度:调整自增起始值控制初始长度。

总结

修复后的方案确保了自增 ID 被正确返回,避免了 NullPointerException 的风险。通过在服务层添加检查和异常抛出,我提高了代码的健壮性。这个设计思路清晰且实用,适合面试展示能力。