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


所有评论(0)