5:创建短链接
ShortLinkCreateReqDTO
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ShortLinkCreateReqDTO {
/**
* 域名
*/
private String domain;
/**
* 原始链接
*/
private String originUrl;
/**
* 分组标识
*/
private String gid;
/**
* 创建类型 0:接口创建 1:控制台创建
*/
private Integer createdType;
/**
* 有效期类型 0:永久有效 1:自定义
*/
private Integer validDateType;
/**
* 有效期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date validDate;
/**
* 描述
*/
private String describe;
}
controller
/**
* 创建短链接
*/
@PostMapping("/api/short-link/v1/create")
@SentinelResource(
value = "create_short-link",
blockHandler = "createShortLinkBlockHandlerMethod",
blockHandlerClass = CustomBlockHandler.class
)
public Result<ShortLinkCreateRespDTO> createShortLink(@RequestBody ShortLinkCreateReqDTO requestParam) {
return Results.success(shortLinkService.createShortLink(requestParam));
}
service
@Transactional(rollbackFor = Exception.class)
@Override
public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
// 短链接接口的并发量有多少?如何测试?详情查看:https://nageoffer.com/shortlink/question
verificationWhitelist(requestParam.getOriginUrl());
String shortLinkSuffix = generateSuffix(requestParam); //生成短链接 - 当布隆过滤器快慢的时候,加上高并发场景下,可能会出现误判
String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
.append("/")
.append(shortLinkSuffix)
.toString();
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.domain(createShortLinkDefaultDomain)
.originUrl(requestParam.getOriginUrl())
.gid(requestParam.getGid())
.createdType(requestParam.getCreatedType())
.validDateType(requestParam.getValidDateType())
.validDate(requestParam.getValidDate())
.describe(requestParam.getDescribe())
.shortUri(shortLinkSuffix)
.enableStatus(0)
.totalPv(0)
.totalUv(0)
.totalUip(0)
.delTime(0L)
.fullShortUrl(fullShortUrl)
.favicon(getFavicon(requestParam.getOriginUrl()))
.build();
ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
.fullShortUrl(fullShortUrl)
.gid(requestParam.getGid())
.build();
try {
// 短链接项目有多少数据?如何解决海量数据存储?详情查看:https://nageoffer.com/shortlink/question
baseMapper.insert(shortLinkDO);
// 短链接数据库分片键是如何考虑的?详情查看:https://nageoffer.com/shortlink/question
shortLinkGotoMapper.insert(linkGotoDO);
} catch (DuplicateKeyException ex) {
// 首先判断是否存在布隆过滤器,如果不存在直接新增
if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
}// --A(1)--
throw new ServiceException(String.format("短链接:%s 生成重复", fullShortUrl));
}
// 项目中短链接缓存预热是怎么做的?详情查看:https://nageoffer.com/shortlink/question
stringRedisTemplate.opsForValue().set(
String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
requestParam.getOriginUrl(),
LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
);
// 删除短链接后,布隆过滤器如何删除?详情查看:https://nageoffer.com/shortlink/question
shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
return ShortLinkCreateRespDTO.builder()
.fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
.originUrl(requestParam.getOriginUrl())
.gid(requestParam.getGid())
.build();
}
/*
A(1):什么情况下会出现数据库中存在完整短链接但是布隆过滤器中却没有?
高并发情况下的竞态条件:
线程A的操作:
线程A接收到生成短链接的请求,并成功生成了一个短链接shortUri。
线程A首先将这个短链接及其相关信息插入到数据库中。
但在线程A将短链接信息插入布隆过滤器之前,线程A可能会被暂停(例如,由于线程调度,I/O 操作等原因),导致该操作延迟。
线程B的操作:
在线程A尚未完成布隆过滤器的更新时,线程B也接收到生成短链接的请求,并尝试生成短链接。
线程B生成了相同的短链接shortUri(由于某些相同输入或随机数种子),然后线程B检查布隆过滤器以判断该短链接是否已存在。
因为线程A还没有将shortUri插入布隆过滤器,所以线程B在布隆过滤器中没有找到该短链接。
线程B接下来也尝试将相同的短链接插入数据库。
结果:
如果数据库对该短链接的唯一性有约束,线程B在插入时可能会遇到数据库的唯一性约束冲突,抛出 DuplicateKeyException 异常。
在这种情况下,线程B意识到该短链接已经存在,并且这个时候才会把短链接信息插入到布隆过滤器中。
这种情况就会导致在短时间内,数据库中存在一个完整的短链接记录,而布隆过滤器中尚未记录这一短链接。
*/
/*
为什么不在生成短链接码的时候就直接插入到布隆过滤器?
应该先将生成好的短链接先插入数据库,后插入布隆过滤器,防止将短链接插入数据库失败,但却将短链接插入到布隆过滤器的错误发生。
于此同时进行操作的时候进行事务处理。插入数据库失败直接回滚。
*/
private void verificationWhitelist(String originUrl) {
Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
if (enable == null || !enable) {
return;
}
String domain = LinkUtil.extractDomain(originUrl);
if (StrUtil.isBlank(domain)) {
throw new ClientException("跳转链接填写错误");
}
List<String> details = gotoDomainWhiteListConfiguration.getDetails();
if (!details.contains(domain)) {
throw new ClientException("演示环境为避免恶意攻击,请生成以下网站跳转链接:" + gotoDomainWhiteListConfiguration.getNames());
}
}
private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
int customGenerateCount = 0;
String shorUri;
while (true) {
if (customGenerateCount > 10) { //加上数量限制,目的是为了防止恶意的攻击,导致一直在死循环中
throw new ServiceException("短链接频繁生成,请稍后再试");
}
String originUrl = requestParam.getOriginUrl();
originUrl += UUID.randomUUID().toString(); //加上一个随机数,可以降低重复率
// 短链接哈希算法生成冲突问题如何解决?详情查看:https://nageoffer.com/shortlink/question
shorUri = HashUtil.hashToBase62(originUrl);
// 判断短链接是否存在为什么不使用Set结构?详情查看:https://nageoffer.com/shortlink/question
// 如果布隆过滤器挂了,里边存的数据全丢失了,怎么恢复呢?详情查看:https://nageoffer.com/shortlink/question
if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shorUri)) {
break;
}
customGenerateCount++;
}
return shorUri;
}
跳转域名白名单配置文件 GotoDomainWhiteListConfiguration
/**
* 跳转域名白名单配置文件
*/
@Data
@Component
@ConfigurationProperties(prefix = "short-link.goto-domain.white-list")
public class GotoDomainWhiteListConfiguration {
/**
* 是否开启跳转原始链接域名白名单验证
*/
private Boolean enable;
/**
* 跳转原始域名白名单网站名称集合
*/
private String names;
/**
* 可跳转的原始链接域名
*/
private List<String> details;
}
< 布隆过滤器误判原理 >
布隆过滤器工作原理:
布隆过滤器可以想象成一个巨大的数组,里面存着的全是01。初始时里面存着的全是0.
当我要插入某个数据的时候,布隆过滤器会使用多个哈希函数来处理这个元素。哈希函数会将这个元素映射到位数组的多个位置,比如位置
i
,j
,k
。 然后在这几个位置将数组内容改为1.
产生误判的原理:
添加A数组的时候,将数组下标:i j k标为1
当查询一个 完全不同的元素 在不在布隆过滤器中的时候,此时打到的数组下标正好是 i j k,显示已经存在过滤器中 。
这种情况大多数存在于布隆过滤器快要满的时候。