1 建表sql

create table t_short_link
(
    id         bigint auto_increment comment 'id'
        primary key,
    flag       varchar(100)                       not null comment '短链标识',
    link       varchar(2048)                      not null comment '原始链接',
    md5        varchar(64)                        not null comment '原始链接的md5',
    created_at datetime default CURRENT_TIMESTAMP null comment '创建时间',
    deleted    tinyint  default 0                 null,
    constraint uk_flag
        unique (flag),
    constraint uk_md5
        unique (md5)
);

2 pom.xml

<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.35</version>
</dependency>

<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version> <!-- 请根据需要选择最新版本 -->
</dependency>

3 mapper层

import biz.taobao.duanlian_1.model.ShortLink;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Optional;

@Mapper
public interface TShortLinkMapper {
    
    /**
     * 根据MD5查找短链接记录
     */
    @Select("SELECT * FROM t_short_link WHERE md5 = #{md5} AND deleted = 0")
    ShortLink find_by_longUrl_md5(@Param("md5") String md5);
    
    /**
     * 根据短链标识(flag)查找短链接记录
     */
    @Select("SELECT * FROM t_short_link WHERE flag = #{flag} AND deleted = 0")
    ShortLink find_by_shortUrl_flag(@Param("flag") String flag);
}

4 核心service层

import biz.taobao.duanlian_1.mapper.TShortLinkMapper;
import biz.taobao.duanlian_1.model.ShortLink;
import cn.hutool.core.util.HashUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 短链接服务实现类。
 * 该服务负责生成唯一短链标志(flag),并提供基于长链接创建短链接的功能。
 */
@Service
@Slf4j
public class Duanlink_Service {
    
    // 分布式锁的键前缀,用于避免重复创建短链接
    public static final String LOCK_SHORT_LINK_CREATE = "lock:short_link:create:";
    
    // 域名前缀,用于生成完整的短链接
    public static final String domainPrefix = "http://www.sina.com.cn/";
    
    @Autowired
    private TShortLinkMapper shortLinkDao;
    
    // 分布式锁客户端,用于处理并发问题
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 将整数转换为62进制字符串表示形式。
     *
     * @param num 输入数字
     * @return 62进制字符串
     */
    private static String to62HEX(int num) {
        num = Math.abs(num); // 取绝对值以确保结果为正数
        
        String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 62进制字符集
        StringBuilder sb = new StringBuilder();
        int remainder;
        
        while (num >= 62) { // 修改:简化条件表达式
            remainder = Long.valueOf(num % 62).intValue(); // 计算当前位
            sb.append(chars.charAt(remainder)); // 添加到StringBuilder
            num = num / 62; // 更新num
        }
        
        sb.append(chars.charAt(Long.valueOf(num).intValue())); // 添加最后一位
        return sb.reverse().toString(); // 返回反转后的字符串
    }
    
    /**
     * 根据长链接生成唯一短链标志(flag)。
     *
     * @param longUrl 长链接
     * @return 短链标志(flag)
     */
    private String generate_shortLink(String longUrl) {
        // 使用MurmurHash32算法对链接进行哈希计算
        int hash = HashUtil.murmur32(longUrl.getBytes(StandardCharsets.UTF_8));
        
        // 将哈希值转换为62进制字符串表示形式
        return to62HEX(Math.abs(hash));
    }
    
    /**
     * 根据ShortLink实体和主机地址生成完整的短链接URL。
     *
     * @param shortLink    ShortLink实体对象
     * @param domainPrefix 主机地址,例如 "http://www.sina.com.cn/"
     * @return 完整的短链接URL
     */
    private String getShortLink(ShortLink shortLink, String domainPrefix) {
        // 参数验证:如果shortLink或host为空,则抛出异常
        if (shortLink == null || StringUtils.isEmpty(shortLink.getFlag()) || StringUtils.isEmpty(domainPrefix)) {
            throw new RuntimeException("生成短链失败");
        }
        
        // 确保host以'/'结尾,如果不是,则添加之
        if (!domainPrefix.endsWith("/")) {
            domainPrefix += "/";
        }
        
        // 返回由host和flag组成的完整短链接URL
        return domainPrefix + shortLink.getFlag();
    }
    
    /**
     * 根据提供的参数创建短链接。
     * 包括检查重复、使用分布式锁避免并发冲突、生成唯一标识符等。
     *
     * @param longUrl 长链接
     * @return 短链接URL
     */
    public String createShortLink(String longUrl) {
        
        // 计算MD5摘要
        String md5 = DigestUtils.md5DigestAsHex(longUrl.getBytes(StandardCharsets.UTF_8));
        
        // 检查数据库中是否存在相同的MD5
        Optional<ShortLink> shortLinkOptional = Optional.ofNullable(shortLinkDao.find_by_longUrl_md5(md5));
        
        // 如果已经存在,则直接返回对应的短链接
        if (shortLinkOptional.isPresent()) {
            ShortLink shortLink = shortLinkOptional.get();
            return getShortLink(shortLink, domainPrefix);
        }
        
        // 使用分布式锁避免相同链接并发处理
        final RLock lock = redissonClient.getLock(LOCK_SHORT_LINK_CREATE + md5);
        
        try {
            // 尝试获取锁,超时时间为3秒
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                // 再次检测是否已有记录(防止在获取锁前已被其他线程插入)
                shortLinkOptional = Optional.ofNullable(shortLinkDao.find_by_longUrl_md5(md5));
                if (shortLinkOptional.isPresent()) {
                    return getShortLink(shortLinkOptional.get(), domainPrefix);
                }
                
                // 生成唯一的短链标志
                String flag = generate_shortLink(longUrl);
                
                // 如果生成的标志已存在于数据库中,则尝试重新生成
                Optional<ShortLink> byShortUrlFlag = Optional.ofNullable(shortLinkDao.find_by_shortUrl_flag(flag));
                if (byShortUrlFlag.isPresent()) {
                    for (int i = 0; i < 10; i++) { // 修改开始
                        // 增加随机后缀重试
                        flag = generate_shortLink(longUrl + RandomStringUtils.randomAlphabetic(10));
                        
                        // 立即检查新生成的标志是否已存在
                        byShortUrlFlag = Optional.ofNullable(shortLinkDao.find_by_shortUrl_flag(flag));
                        if (!byShortUrlFlag.isPresent()) { // 若新生成的标志未被使用,则退出循环
                            break;
                        }
                    }
                    
                    // 如果经过多次尝试后仍未能获得唯一的flag,可能需要采取其他措施或抛出异常
                    if (byShortUrlFlag.isPresent()) {
                        throw new RuntimeException("无法生成唯一的短链接标志"); // 修改结束
                    }
                }
                
                // 插入新的短链接记录
                ShortLink shortLink = new ShortLink(flag, longUrl, md5, (byte) 0);
                shortLinkDao.insert(shortLink);
                
                // 返回最终的短链接
                return getShortLink(shortLink, domainPrefix);
            }
        } catch (Exception e) {
            // 如果是中断异常,则恢复中断状态
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            log.error("[short_link] 短链生成失败", e); // 日志记录错误
            
            throw new RuntimeException("生成短链失败"); // 抛出自定义异常
        } finally {
            // 确保释放锁
            if (lock.isHeldByCurrentThread() && lock.isLocked()) {
                lock.unlock();
            }
        }
        
        // 如果所有尝试均失败,则返回原始链接
        return longUrl;
    }
}

5 controller层

import biz.taobao.duanlian_1.mapper.TShortLinkMapper;
import biz.taobao.duanlian_1.model.ShortLink;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

@RestController
@Slf4j
public class DuanlinkController {
    
    @Autowired
    private Duanlink_Service duanlinkService;
    
    @Autowired
    private TShortLinkMapper shortLinkDao;
    
    /**
     * 生成短链接
     * @param longUrl
     * @return
     */
    @PostMapping("/create")
    public ResponseEntity<String> createShortLink(@RequestParam String longUrl) {
        try {
            // 调用服务层生成短链接
            String shortLinkUrl = duanlinkService.createShortLink(longUrl);
            
            // 返回生成的短链接URL
            return ResponseEntity.ok(shortLinkUrl);
        } catch (Exception e) {
            // 记录错误日志
            log.error("[short_link] 短链生成失败: {}", e.getMessage());
            
            // 返回错误信息
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + e.getMessage());
        }
    }
    
    /**
     * 通过短链接来访问
     * @param flag
     * @return
     */
    @GetMapping("/{flag}")
    public ResponseEntity<Void> redirect(@PathVariable("flag") String flag) {
        // 查找短链接对应的原始链接
        ShortLink originalLink = Optional.ofNullable(shortLinkDao.find_by_shortUrl_flag(flag))
                .orElseThrow(() -> new RuntimeException("找不到该短链"));
        
        // 获取原始链接
        String locationUri = originalLink.getLink();
        
        // 返回302状态码及Location头部,告知客户端重定向到原始链接
        return ResponseEntity.status(HttpStatus.FOUND)
                .location(URI.create(locationUri))
                .build();
    }
    
}

6 测试,本地测试,可以测试下host文件,
效果如下:

长链接
http://www.zhongyoo.com/name/songhuafen_112.html
转成短链接
http://www.sina.com.cn/yIiQZ

部分逻辑,解释如下

示例 1: 成功生成唯一的短链标志
假设有一个长链接 http://www.zhongyoo.com/name/banxia_984.html:
首次尝试:使用 generate_shortLink 方法基于该长链接生成一个短链标志(例如 abc123)。
检查数据库:查询数据库,发现该短链标志尚未被使用。

结果:直接将此短链标志与主机地址组合,生成最终的短链接,并保存至数据库。在这个例子中,不会走到添加随机字符串的部分,因为初次生成的短链标志是独一无二的。

示例 2: 需要重新生成短链标志(发生冲突)
假设我们有两个不同的长链接 http://www.zhongyoo.com/name/banxia_984.html 和 http://www.zhongyoo.com/name/yudancao_13537.html,它们恰好生成了相同的短链标志 abc123:
为长链接A生成短链:
使用 generate_shortLink 方法基于 http://www.zhongyoo.com/name/banxia_984.html 生成短链标志 abc123。
检查数据库,发现 abc123 尚未被使用,成功生成短链接并保存。

为长链接B生成短链:
同样使用 generate_shortLink 方法基于 http://www.zhongyoo.com/name/yudancao_13537.html 生成短链标志,但这次也得到了 abc123。
检查数据库,发现 abc123 已经被长链接A使用了。
在这种情况下,程序会进入重试逻辑:
处理冲突:在循环中,首先拼接原始长链接和一个随机字符串(如 randomString),形成新的输入 http://www.zhongyoo.com/name/yudancao_13537.htmlrandomString。
使用 generate_shortLink 方法基于新的输入重新生成短链标志(例如 def456)。
再次检查数据库,如果 def456 未被使用,则停止循环并使用这个新的短链标志。
结果:将新生成的短链标志 def456 与主机地址组合,生成最终的短链接,并保存至数据库。

关键点总结
正常情况:大多数情况下,首次生成的短链标志都是唯一的,不需要进行额外的重试。
异常情况:当首次生成的短链标志已经存在于数据库中时(即发生了哈希碰撞),就需要通过增加随机字符串的方式重新生成短链标志,直到找到一个唯一且未被使用的标志为止。
机制保障:即使在极小概率下出现哈希碰撞的情况下,系统也能自动生成唯一的短链标志,避免重复问题。
在这种方式下,程序确保了即使在哈希碰撞发生时,也能通过附加随机字符串的方法解决冲突,保证每个短链标志的独特性。这种方法虽然不能完全排除所有可能的冲突,但在实际应用中已大大降低了这种可能性。

更多推荐