- 第13节:短链接信息修改
project\...\ShortLinkCreateReqDTO.java和remote\..\ShortLinkCreateReqDTO.java的有效期是添加注解
@JsonFormat(pattern = "yyyy-MM-dd MM:mm:ss",timezone = "GMT+8")
这个有效期类型做成枚举后面我们做一些判断是好判断的,shortlink.project.common.enums里面添加
package com.nageoffer.shortlink.project.common.enums; import lombok.Getter; import lombok.RequiredArgsConstructor; /** * 有效期类型 */ @RequiredArgsConstructor public enum VailDateTypeEnum { /** * 永久有效期 */ PERMANENT(0), /** * 自定义有效期 */ CUSTOM(1); @Getter private final int type; }
我们开始在project中新建ShortLinkUpdateReqDTO和controller层
package com.nageoffer.shortlink.project.dto.req; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.util.Date; /** * 短链接修改请求对象 */ @Data public class ShortLinkUpdateReqDTO { /** * 原始链接 */ private String originUrl; /** * 完整短链接 */ private String fullShortUrl; /** * 分组标识 */ private String gid; /** * 有效期类型 0:永久有效 1:自定义 */ private Integer validDateType; /** * 有效期 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date validDate; /** * 描述 */ private String describe; }
@PutMapping("/api/short-link/v1/update") public Result<Void> updateShortLink(@RequestBody ShortLinkUpdateReqDTO requestParam) { shortLinkService.updateShortLink(requestParam); return Results.success(); }
service自行添加, impl修改如下
@Transactional(rollbackFor = Exception.class) @Override public void updateShortLink(ShortLinkUpdateReqDTO requestParam) { LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, requestParam.getGid()) .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl()) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0); ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper); if (hasShortLinkDO == null) { throw new ClientException("短链接记录不存在"); } ShortLinkDO shortLinkDO = ShortLinkDO.builder() .domain(hasShortLinkDO.getDomain()) .shortUri(hasShortLinkDO.getShortUri()) .clickNum(hasShortLinkDO.getClickNum()) .favicon(hasShortLinkDO.getFavicon()) .createdType(hasShortLinkDO.getCreatedType()) .gid(requestParam.getGid()) .originUrl(requestParam.getOriginUrl()) .describe(requestParam.getDescribe()) .validDateType(requestParam.getValidDateType()) .validDate(requestParam.getValidDate()) .build(); if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) { LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class) .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl()) .eq(ShortLinkDO::getGid, requestParam.getGid()) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0) .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null); baseMapper.update(shortLinkDO, updateWrapper); } else { LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class) .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl()) .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid()) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0); baseMapper.delete(updateWrapper); baseMapper.insert(shortLinkDO); } }
有一个点需要确定,我们这里面有什么?有一个创建短链接和一个创建链接的时候,我们可以指定它的有效期。如果说我们要干什么?我们给它设置成永久有效,有效期就要需要是空的,所以说我们需要给他做一点额外的判断,然后是 website。第二 update。Also link plus I think they'll get shot it for a while, I got to put it at the 14. 这里面的话我想一想,我们看一下它返回的是有没有一个for UI字段。有,我就把这个拿过来。我想一想,放下以后它是不允许变更的。没关系。我们的域名不允许它变,域名不允许它变。然后这边的话是先复制完整版链接。放出来,然后这里还有一Q一下,别忘了我们是通过什么分组的。So click back决定,然后就开始给他界定。对我们要有一个set如果说。如果说我们的 request的getValidDateType等于我们新创建的这个枚举。永久有效就是getValidDateType.PERMANENT。那么我们就要干什么?我们就要给有效期设为null,然后点击updateWrapper.setValidDate(null)。这时候应该就不用点了。然后我们要给它设置一个点EQ,getDelFlag等于0,然后点击eq(ShortLinkDO::getEnableStatus, 0),ok。然后我们去给他做一个 snip第二类词。不能叫就 that update外婆。然后我们去给它这个是上面这个是条件,我们要给它在这里去给它创建一个ShortLinkDO肯定有点。有的地儿有的尸体在前面。然后点第一个就是 gid。我想想再次感谢GD,没关系,我们先这么玩儿。然后第二个设置的它的原始链接肯定是可以要变的,然后是它的描述,然后再接下来就是它的有效期的类型,拿出它这个有效期。Ok好像就这些。但是这里面会有一个问题,如果说我们什么?我们获取到的短链接对吧?它要切换分组的话,我们这么做就有问题了,因为你切换分组我们说实话是用分组去做的,我们是用分组去做的分表对吧?我们用的组做分表,如果说你切换了分组的话,它在原来的表里面就找不到了,所以说我们要先给它做一个删除记录。如果说,他的现在传递的分组和他原来的分组匹配不上,那就要先删除。Ok首先我们要先查询,要先做一个查询,我们看一下我们之前查询是怎么做的,现在应该还没有那个查询,ok没关系,我们先写一个,然后外婆4点那么的carry决定软件,然后再去queryWrapper.eq(gid)。说什么,然后把这两个EQ拿过来,然后做一个贝斯迈克尔第二。Flight one, 然后20万 soft link, soft link deal. 然后if如果shortLinkDO不等于null,然后那个点这里面会有一些缓存,会有一些穿透性的问题,稍后会跟大家讲,先把功能完成。然后第二get它的gid。好吧,这个是第二个就是gid,然后和一个快速的getGid进行比对如果说一致,我们走这个更新逻辑。如果说不一致,干什么?我们首先要给它瑞木再给它做set,对不对?然后我们这边给他搞一个,这边的话就得给他单独的拎出来。我想一下,好像不用拎,我们这边的话就不这么玩了,我们就不能这么玩了,我想想。如果这样的话,我们这边得把什么得把然后其他的参数我们要给他去获取到之前的,比如说当然我们要给他获取到配色上的的哥们,然后有一个少的UI,然后黑色对他说的UI,然后点一个再点出来一个这个number可以稍微要点这个number点,一个它等会大家就明白我为什么要这么写了。还有一个点可是的态度。应该这样。然后这边的话就不能把它写到这里,得给它放出去。因为如果说不一致的话,不一致我们首先要干什么?先删。点 delete, 我首先要delete,delete的话这个东西 delete我们要通过这种方式去给它delay它,然后delay了之后我们还要干什么点?点insert,insert的话我们还是用相当于这两边做了一个互用。我们检查一下代码,EQ, 如果说它不等于空,应该这样。应该直接就if黑色的等等。我觉得思路用一个so is exception来解决一个看一个section,短链接不存在,然后这样的话我们不就少了个层级对不对?代码重构的知识又加一了。看着是没问题的,然后这里面需要加一个加一个这个数有百克,然后有30的plus。因为我们这里面涉及到一个先删再新增了,所以说要加一个600克。然后不对,这里还有点问题,这个giddy。如果说, gide的话它是相当于现在是request里面传的。我就在这里给它改一下。代码不能写错。State C加D那就是从我想一想, C加D的话默认应该要拿如果说,他俩一致的话拿哪个都行。一致的话直接就稍等我想一下。对一致的话他他俩拿哪个都行,然后如果不一致,我们这里面要给他设置一个这里不能用快死了,应该用我们的然后用他的相当于先用原表的方式去删完之后,我们这里面比较site,决定应该是我们的快手里面的建立,然后如果说两个一致,他去的是如果两个一致的话,它这里面直接就用两个的软用筷子代替就行了,如果说两个不一致,那我们就得去 request里面的。再捋一下如果说,我们传过来的G ID是修改操作,那就相当于和 history link里面是一样的,我们这里面去给他设计一个第二 G ID我们这边直接就取什么?直接就取黑色的,要get G ID是可以的,对吧?因为他俩如果是修改操作他俩应该一致的话,我们这里面就不用再单独的set了,然后我们直接把它拿过来,但是如果说我们这两边不一致,我们如果是新增操作,我们这里面它如果是新增,我们应该去快速的里面,不对,它应该先去 case link里面去给它删掉,对吧?然后删掉之后再取水,快速的点get URL,get Gad,没问题。然后证明了jad首先它是第一个它是要用来做一个修改的话,用两个哪个都可以,然后如果说不一致,那不用我们这边直接设置一个快速的就行了,然后就不用这样搞了,如果两个都一致对吧?他俩是个修改操作,那么他直接就用了如果说不一致,ok他取得也是这个request里面的request就是我们新参数里面的也没问题,可以这样。
feature:短链接服务开发修改功能
admin也要有updateDTO,两个ShortLinkController.java都添加
/** * 修改短链接 */ @PostMapping("/api/short-link/admin/v1/update") public Result<Void> updateShortLink(@RequestBody ShortLinkUpdateReqDTO requestParam) { shortLinkRemoteService.updateShortLink(requestParam); return Results.success(); }
ShortLinkRemoteSeervice添加如下
/** * 修改短链接 * * @param requestParam 修改短链接请求参数 */ default void updateShortLink(ShortLinkUpdateReqDTO requestParam) { HttpUtil.post("http://127.0.0.1:8001/api/short-link/v1/update", JSON.toJSONString(requestParam)); }
短feature:链接后管开发短链接修改功能
在数据库t_Link中选一个你当前账号的短链接,可以进行修改
{
"fullShorturl": "http://eve2333.com/4R6e8Y",
"originurl": "http://nageoffer.com",
"gid": "14",
"validDateType": 1,
"validDate": "2025-01-01 11:45:14"
}
或者
{
"fullShorturl": "http://eve2333.com/4R6e8Y",
"originurl": "http://nageoffer.com",
"gid": "3TajjG",
"validDateType": 0,
"describe":"阿莫西西多mkmkmmk"
}
feature:修改短链接接口网络方法类型
修改短链接 gid 存在的问题已在【功能扩展@短链接变更分组记录功能】章节修复了
Shimmer.:这里只传进来了一个用于查询的gid,没有传进来修改的gid
2023-11-14 22:05
1
可以补兵但没必要 回复 Shimmer.:感觉修改分组跟修改数据不能同时进行
2023-11-21 14:48
1
王小明 回复 可以补兵但没必要:是这样的 实际业务里改分组就不会改数据,甚至改数据都是只改1-2个字段,不会像这种每次都传一堆参数
2024-01-01 23:43
champagnep* 回复 王小明:但是你看一下小码短链接就是可以同时改分组和数据
Binding:根据传过来的参数gid,shortLInkUrl.......查询数据,能查到数据的话,那为什么查出来记录的gid和传过来的gid不一样呢?if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid()))这行代码
2023-11-17 11:06
2
vzer 回复 Binding:我的理解是,传过来的requestPrarm的gid是修改后的gid,查出来的gid是原始记录的gid
2023-12-22 14:45
3
苏格拉没有底ᥫᩣ 回复 vzer:这段代码难道在这里面不是恒成立的吗,根本不会走下面的分支。
2023-12-25 21:45
青青子衿 回复 苏格拉没有底ᥫᩣ:我也有这个疑问,朋友你弄明白了吗?
2024-01-28 00:34
。。。。。。 回复 青青子衿:同有问题,既然能查到那requestParam.gid肯定和查到的hasShortLinkDO.gid相同啊?又做这个判断无法理解
2024-01-30 19:13
pp 回复 。。。。。。:最新的代码改了,加了一个原始gid字段,最新的代码用的是原始id
2024-07-15 21:35
。 回复 苏格拉没有底ᥫᩣ:是的,后面修正: /** * 原始分组标识 */ private String originGid; /** * 分组标识 */ private String gid;
2024-12-29 15:00
既然请求参数中的OriginUrl可能会变, 那图片也可能会变吧
是的。不过因为图片非核心流程,就不处理了。有兴趣可以提交个 PR
目的是修改短链接信息,我觉得视频里这个写法应该是有点问题的,然后看了下马哥的最新源码,以下是参照最新源码说的。 ①首先查询数据库中是否存在要修改的短链接,若不存在直接抛出异常结束流程 ②比较数据库中的gid和本次传入的gid是否相同(注意,传入参数有两个gid,分别是原始gid和新gid,原始gid用于第一步的查询操作,新gid用在这一步) 若相同,则直接更新即可(basemapper.update),若gid不相同,则需要删除这条记录再新增,原因在于:短链接库使用gid作为分片键进行分库,若gid被修改,则不能按照原来的规则通过gid找到这条记录,因此需要把原记录删除,然后再把新gid作为gid创建一条新的短链接记录
王小明:短链接信息修改这个业务,并不能修改全部信息,一般只可以修改短链接的**分组**,或修改短链接的**描述**,或者修改短链接的**有效期**(续费业务),或修改短链接的**原始链接**(即跳转链接); **短链接url**一般是不给修改的,因为都是随机生成的6位字符,从业务上来说没有修改的意义; 短链接的**域名**用户可以自行提供或使用网站服务默认的域名,域名在创建后也会固定不可修改. 参考:小码短链接
2024-01-02 13:43
1
大笨猪 回复 王小明:感觉老哥讲的挺好,我有点个疑问,为什么马哥不将修改分组信息和修改其他信息作为两个接口来写呢,我感觉在实际业务中这应该也是两个不同的操作才对的
2024-01-13 15:04
王小明 回复 大笨猪:是的 实际业务中还是分开写吧,这里可能就是想早点实现功能
2024-01-13 15:05
大笨猪 回复 王小明:老哥,还想问你个问题,马哥之前写的每个用户下的gid是唯一的,但是不同用户的gid是可以重复的,这样子在通过link表查询短链接的时候,不是会把其他用户的短链接也查找出来吗,这个写法好像问题挺大的呀,还是我理解错误了呢?
2024-01-13 15:07
王小明 回复 大笨猪:确实存在这个问题,不同用户可以有相同的gid,gid应该是全局唯一而不是用户下唯一,因为6位的gid可以生成568亿种组合,完全够使用了,像小码短链接的分组gid,更是使用8位来生成,数量更是绰绰有余,所以我建议用一个布隆过滤器,来判断gid是否已经生成过,就和username一样。另外,即使gid全局唯一,目前的代码也是可以查别人gid下的短链接,因为没有判断一下gid是否属于当前用户,对于其他的修改业务,创建业务也同理,建议还是加上,虽然8位gid不容易猜出来,但用户信息泄露总是不好的。
不过这些都属于细节上的东西了,项目又不会上线使用,修改不修改,看你的想法了
2024-01-13 15:14
大笨猪 回复 王小明:好的,感谢老哥
2024-01-13 15:31
TheTurtle 回复 王小明:我有个疑惑,这种先删后插入的方式合适吗?因为表 id 一般是自增的,删除再插不会导致超过最大值吗?
有个疑问:不是可以修改gid,也就是分组信息吗,然后接口里面一开始通过gid跟完整短链接来查询是否存在,那如果修改了gid,传进来的不就是一定是找不到的吗,这样不就是找不到短链接,那不就是修改不了短链接分组了吗
补充一个点:如果修改分组后,再次修改回原来的分组,会和逻辑删除的记录完整短链接是一样的,不能直接新增,感觉需要覆盖。唯一索引加了个 full_short_url,del_time,最后的 del_time 能保证不一致
除了评论区其它同学的问题,看到这我还是有一个问题不知道后面解决了没有,跟短链接查询有关。短链接表理论上应该指派一个username字段吧?光凭借gid和分组表关联这个关系是不是太弱了?分组表里面我记得username + gid是全局唯一的,而且gid的生成逻辑也是判断一下username和生成的gid没在表中出现过就合法,那就说明不同user有同样的gid的情况?那只要gid一样我查询短链接的时候岂不是把别人建的短链接也查过来了?还是我理解错了?有知道的同学踹我一下。
你说的方法是可行的,那就是 gid + username 保障全局唯一。但后面为了减少表结构调整,通过 gid 全局唯一方式解决上述问题
- 第14节:短链接跳转原始链接功能(上)
跳转原理
通过短链接获取到对应的长链接,对长链接进行 302 重定向,最终访问原始网址。
跳转流程
- 缓存怎么办?访问缓存和数据库不存在短网址,数据库压力骤增。
- 短时间内大量请求访问一个缓存过期短链接,那么就会造成缓存击穿。
接下来完成短链接跳转功能,首先一般情况下我们都是网站里面直接域名后加 short-uri,project新增的shortcontroller如下
@GetMapping("/{short-uri}") public void restoreUrl(@PathVariable("short-uri") String shortUri, ServletRequest request, ServletResponse response) { shortLinkService.restoreUrl(shortUri, request, response); }
/** * 短链接跳转 * * @param shortUri 短链接后缀 * @param request HTTP 请求 * @param response HTTP 响应 */ void restoreUrl(String shortUri, ServletRequest request, ServletResponse response);
@SneakyThrows @Override public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) { String serverName = request.getServerName(); String fullShortUrl = serverName + "/" + shortUri; LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl); ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper); if (shortLinkGotoDO == null) { // 严谨来说此处需要进行封控 return; } LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid()) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0); ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper); if (shortLinkDO != null) { ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl()); } }
要判断第一点,判断当前的这个是否存在于布隆过滤器,怎么去做,我们看一下我们新增的时候我们有干什么,我们有给它往不同过滤器里面去添加,但是添加的是一个sub face。Subface的话应该也是可以的,但是这里应该不太行。我想一想,这里的话我们就不能只添加 double face,我们干什么?我们要把域名也给它加上,我们要把给它 photo UI给它传进去,没毛病。对,这里面改了一下,把报销的要给传进去,因为我们一个短网址是能够在多个域名里面去用的,所以说这里给它加进去,对没问题对,没问题。相当于他去判断是否存在的时候,我看一下。因为我想帮你看一下,然后就可以了。去判断,在这里继续给他做一个创建的时候是判断的,他加上没问题,然后我们看一下创建的时候,他传的ok带HTTP了。在这里首先我们要通过什么布隆过滤器。第二,电视稍有I,当然不能直接用上头,我们要先通过什么?我们要先通过这个request,request点get一个server name。Get一个server HTTP let request,然后首先用get一个server。我去。你们get不出来了。不会是这个吧?搞错了,怎么搞成糊涂下面去了?Request. Several night. It request. 我们看一下,应该是。So let HTTP request. 但是不太对,我们应该是有一个HTTP搜出来的才对。我想一下 htp. 不对,肯定不是糊涂的。他应该是在我看一下,我们之前应该是在公共的科目里面有给他拿对应的一个东西,应该是 let request,然后再给它强转成我们先so let的快死了,我看这个时候来的时候能不能get出来一个。So name可以他也行。 That response. Let's request let the response拿到一个他get server。But okay. Get your server. 怎么回事? Let request. Yes. 忽略了这边没给他提上去。但是我然后solo name加上一个叫什么?一个杠再加上一个 shut UI. 然后等于four shot us。来判断它是否存在。
If如果说不存在,它不存在也会可能有一些误判,我们就先不判断这个了。我们先通过去获取对应的开始。首先拿到原始路径,我们拿到原始路径的话,我们先以最简单的方式去写,比如说最简单的什么,然后最简单的我看一下直接找一个查询方法,看能不能找得到。我们先找一个查询方法,ok在这里拿过来,然后我们在这里面找一个 gid问题来了,这个就是我想跟大家讲的,我们这里发现大家发现没有,我们对应的 link,表里面它只是通过gid进行分库分表的,也就是我们的分组标识。现在用户能传给我们的只有一个shot UI,shot UI也就相当于你看假如说我们的是什么N URL,然后点nk-123456,它只能传给我们一个这个东西,我们只能通过短链接去找对应的原始网址,这个时候是没有gid的,所以说这个时候不得不引出一个东西,就是叫做路由表的概念,什么意思?相当于我们创建一个路由表,我给大家去看一下,我们创建出来一个这个东西,叫做K-linK-go to。然后也是begin,什么意思?很简单,相当于我们在创建一张表,这张表是通过 short shot URL去进行分组,去进行分片,然后里面再存一个啥?再存一个gid,然后这样的话我们就能够拿到分组,再去找我们对应的拿到我们的 shot UI,然后去找我们对应的G ID就可以了。也是应该是four shot12。这样。Ok,这样子就可以了。我给大家操作一下。在这里面我们相当于得把这个表一样创建很多的创建很多这样的表
package com.nageoffer.shortlink.admin.test;
public class UserTableShardingTest {
public static final String SQL = "CREATE TABLE `t_link_goto_%d` (\n" +
" `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',\n" +
" `gid` varchar(32) DEFAULT 'default' COMMENT '分组标识',\n" +
" `full_short_url` varchar(128) DEFAULT NULL COMMENT '完整短链接',\n" +
" PRIMARY KEY (`id`)\n" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
public static void main(String[] args) {
for (int i = 0; i < 16; i++) {
System.out.printf((SQL) + "%n", i);
}
}
}
我们在创建短链接就把他也创建出来了,相对于我们这里面去获取原始短链接的时候,首先我们要去哪数据库里面去查,然后他首先要通过通过短网址找到GID,通过GID找到原始网址。
你为什么不在数据库t_link_goto中加一个对应的长网站就ok了啊?不建议这样做啊,虽然能够减少一次寻址的机会,因为你相当于之前改一个地方就行了,现在要改两个地方,但是也行
project里面创建对应的mapper
package com.nageoffer.shortlink.project.dao.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.nageoffer.shortlink.project.dao.entiry.ShortLinkGotoDO; /** * 短链接跳转持久层 */ public interface ShortLinkGotoMapper extends BaseMapper<ShortLinkGotoDO> { }
project里面创建实体
package com.nageoffer.shortlink.project.dao.entiry; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * 短链接跳转实体 */ @Data @Builder @TableName("t_link_goto") @NoArgsConstructor @AllArgsConstructor public class ShortLinkGotoDO { /** * ID */ private Long id; /** * 分组标识 */ private String gid; /** * 完整短链接 */ private String fullShortUrl; }
创建对应的分表规则,修改shardingsphere-config
rules: - !SHARDING tables: t_link: actualDataNodes: ds_0.t_link_${0..15} tableStrategy: standard: shardingColumn: gid shardingAlgorithmName: link_table_hash_mod t_link_goto: actualDataNodes: ds_0.t_link_goto_${0..15} tableStrategy: standard: shardingColumn: full_short_url shardingAlgorithmName: link_goto_table_hash_mod shardingAlgorithms: link_table_hash_mod: type: HASH_MOD props: sharding-count: 16 link_goto_table_hash_mod: type: HASH_MOD props: sharding-count: 16 props: sql-show: true
我们在impl里面查询对应的gid,Wrappers.lambdaQuery,然后shortLinkgotoDO点class.EQ。一个所有的原料。然后在这里面的话,我们给它创建一个叫做gotoLink
@SneakyThrows @Override public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) { String serverName = request.getServerName(); String fullShortUrl = serverName + "/" + shortUri; LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl); ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper); if (shortLinkGotoDO == null) { // 严谨来说此处需要进行封控 return; } LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid()) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0); ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper); if (shortLinkDO != null) { ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl()); } }
因为我们本身它其实不够进行转发的,因为它需要配合nginx以及生产正式的域名,所以说我们可以干什么我们可以通过本地host的文件的形式来给它进行一个跳转,比如说我们host文件绑定一个
nurl.ink 127.0.0.1
mac和linux如上,如果你是windows,记得反一下,挂梯子会对host的映射有影响把你的clash什么的关了
{
"domain": "http://eve2333.com",
"originUrl": "https://witty-heartache.com/20250601",
"gid": "1k91Uw",
"createdType": 1,
"validDateType": 1,
"validDate": "2025-01-01 00:00:00",
"describe": "啊米浴说的道理ccb78物业"
}
编辑
* 因为前端请求过来是短链接,所以需要根据短链接找到原始链接,然后重定向到原始链接 * 但是t_link是根据gid来分片的,所以需要先根据短链接找到gid,然后再根据gid和fullShortUrl找到原始链接 * 新建一个路由表t_link_goto(使用full_short_url分片),用于存储短链接和gid的映射关系,从而再检查到gid * 再求查询t_link中的原始链接
我个人认为还是将短链接本身作为分片键好一些。因为根据分组查询短链接的频次,要远小于短链接跳转操作的频次,同时后者对于接口响应时间的要求也更高。
但是其实我觉得分组查询很直觉,用户登录上来,肯定会查每个分组,再查每个分组下的短链接;如果按短链接作为分片键,查每个分组下的短链接需要把所有表都遍历一次了,数据多的时候挺耗时的。短链接跳转查询时间的问题由路由表可以得到控制,还可以加缓存,从时间复杂度上来说马哥的做法显然更快。
更新短链接时,如果修改了gid,那么goto表是不是也应该同步修改?是的,最终都改了的
为什么不建议直接做短链接到原始连接的路由
是可以的,不过这样的话,相当于变更也需要操作两个表。而且,大部分情况下,因为有缓存的存在,基本上不会查询路由表。两个方案各有利弊,我只是用了现在的方案
对于这种中间表在创建的时候不应该加外键约束吗?外键一般不会加的,通过代码逻辑和事务约束就好
- 第15节:短链接跳转原始链接功能(下)
我们正常去访问这个跳转接口的时候,我们是不访问后管的,直接到我们的中台应用。然后在中台应用里面
我们需要知道它是HTTP或者HTTPS的吗?如果说它是HTTP或者HTTPS它其实主要解析是在NGINX那边去解析,解析完成之后会跳转到我们这边,其实我们不需要知道http,https。那么修改方法应该修改,不能再传HTTP和HttPS了
创建短链接里面不加http:// "domain": "baidu.com",
{
"domain": "nurl.ink",
"originUrl": "https://nageoffer.com",
"gid": "1k91Uw",
"createdType": 1,
"validDateType": 1,
"validDate": "2025-06-01 00:00:00",
"describe": "唱山歌勒哎哎哎666"
}
得到nurl.ink/4ZMjRi
相当于还得查一遍那个域名表,要知道http和https是哪个,我们要展示给前端,我们是否可以让他传的时候传过来 然后我们去给ShortLinkCreateReqDTO里面加一个
/** * 协议 */ private String domainProtocol;
这个在下面又删除了,事实上直接http+就行了,毕竟是两个统一的,让DNS自己跳
shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl); return ShortLinkCreateRespDTO.builder() //.fullShortUrl(shortLinkDO.getFullShortUrl()) .fullShortUrl(requestParam.getDomainProtocol() + shortLinkDO.getFullShortUrl()) .gid(requestParam.getGid()) .originUrl(requestParam.getOriginUrl()) .build();
.enableStatus(0)
.fullShortUrl(fullShortUrl)
.build();
ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
.fullShortUrl(fullShortUrl)
.gid(requestParam.getGid())
.build();
try {
baseMapper.insert(shortLinkDO);
shortLinkGotoMapper.insert(linkGotoDO);
其实没必要让前端传对吧,我们后续啊会做一个域名管理的一个功能:我们会根据前端传的域名来判断他是否就是他的当前这个用户是否可以用这个域名;在他判断域名是否存在的时,把这个域名记录给他拿出来,进而拿到他这个协议就好了。
两个ShortLinkCreateRegDTO.java 删除domainProtocol; impl里面换回这个
return ShortLinkCreateRespDTO.builder() //.fullShortUrl(shortLinkDO.getFullShortUrl()) .fullShortUrl("http://" + shortLinkDO.getFullShortUrl()) .gid(requestParam.getGid()) .originUrl(requestParam.getOriginUrl()) .build();
refactor:查询短链接分页时拼接HTTP协议
@Override public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) { LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, requestParam.getGid()) .eq(ShortLinkDO::getEnableStatus, 0) .eq(ShortLinkDO::getDelFlag, 0) .orderByDesc(ShortLinkDO::getCreateTime); IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam, queryWrapper); //return resultPage.convert(each -> BeanUtil.toBean(each, ShortLinkPageRespDTO.class)); return resultPage.convert(each -> { ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class); result.setDomain("http://" + result.getDomain()); return result; }); }
为啥nurl.ink/加上对应的6位短码访问不了程序啊,必须要加个端口号8001 用完整的nurl.ink:8001/才能访问程序吗?可我们做的功能是用户访问nurl.ink/加上对应的6位短码就可以跳转啊,这里不是很懂
一般来说不加端口就是默认 80,需要 nginx 代理跳转,因为我们没有使用 nginx,所以这里直接加上端口
debug的时候发现有些时候的请求shortUri会传成favicon.ico,这是什么原因呀 ?
开始也有这个问题,后面仔细debug发现在impl类的restoreUrl方法中,我把图中TODO包裹的方法误写成了getShortUri。改正过之后就可以完成页面跳转了
- 第16节:短链接跳转原始链接功能(缓存击穿)
正如你所见,restoreurl里面是直接eq数据库的,应该是不是要先去让他去查缓存
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
constant里面新建RedisKeyConstant
package com.nageoffer.shortlink.project.common.constant; /** * Redis Key 常量类 */ public class RedisKeyConstant { /** * 短链接跳转前缀 Key, %s就是域名对应的短链接 */ public static final String GOTO_SHORT_LINK_KEY = "short-link_goto_%s"; /** * 短链接跳转锁前缀 Key */ public static final String LOCK_GOTO_SHORT_LINK_KEY = "short-link_lock_goto_%s"; }
然后如果说原始link等于空,然后等于空的时候,这个时候我们不是要去查了对不对?查的时候,那如果说缓存击穿是怎么回事?就是一个key刚好失效或者说被删除了,然后这个时候有大量的请求去查询 key,这样的话就全部用到数据库对不对?
if (StrUtil.isNotBlank(originalLink)) { ((HttpServletResponse) response).sendRedirect(originalLink); return; }
这样的话我们要干什么?我们要用分布式锁redissonClient, 然后在这点get out。Get lock的话,我们这里面; 然后我们在这里面去干什么还是老样子,String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl),然后再加上一个fullShortUrl。所以然后获取到这把锁。大家都知道有看门狗的功能,它有非常多特性,然后性能比较高,所以说我们日常中直接用redisson就好了,然后我们就直接让他去获取,然后lock,我们就直接在这里面去try,然后lock。
然后的话我们这里面再看一下如果说,举个例子,如果说有1000个请求,这个时候全部都达到了 lock的lock对吧?这样的话他们肯定最终都会获取到锁对不对?他们都能拿到锁,是不是都要执行一遍数据库的查询方法,是不是没有必要?因为只有第一个请求来了之后对吧?他把这个数据库把这个数据再加载到缓存里面,我给大家演示一下,然后我们要干什么,我们要给他 ops value.
stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()); ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
然后这样的话他下次再来的时候,这里面就能直接get到了,然后我们这里面已经设置好了,相当是不是有一个什么,因为分布式组是挨个来的,对不对?我们第一个只要设置好是不是?第二到甚至到后续所有的请求都没必要再去执行了,这个时候就会有一个东西叫双重判定锁 , 什么意思?双重判定锁就是在这里我再给你执行一次originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl)); ,就相当于我拿了锁之后我再执行,我看看到底有没有,如果没有的话我再去执行好吧?节省一些数据库的响应.
我们后续再重构。这个观点。然后我们在else里面直接给他干什么,直接给他进行调整,不等于空,然后把它设置进去,然后这样的话相当于它上面小的领口点,我把这个给它放出来,然后可以讲直接有一个is not black,然后在这里,然后第二点然后把这个东西抽出来,这不就行了。这样起来看着优雅一点,其实还可以再重构
好吧?然后我们看一下有没有问题。首先拿到原始的链接,如果判断原始链接点空,那么它就要去创建原始链接,这里还有一点问题,如果说原始链接等于空,如果不等于空的话,它需要去给它去做一个什么?去做一个这个东西。Ok我们首先在这里面再来看一下,然后点is not black。Ok,然后把这个东西抽出来,抽出来之后这样子。然后我们看一下,然后把这个东西跳转出来,跳转出来之后,然后如果说他拿到锁,拿到锁之后it's not black。然后他这边也看一个那天他去跳转如果说等于空的话,然后这里面就开始 return,我说不等于空。Ok没问题,这个代码没问题。大家可以自己接下来自己可以去你debug一下,然后在这之前我想说的是我们可以试一下,然后我们给它重启一下
@SneakyThrows
@Override
public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
String serverName = request.getServerName();
String fullShortUrl = serverName + "/" + shortUri;
String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
if (StrUtil.isNotBlank(originalLink)) {
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
lock.lock();
try {
originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
if (StrUtil.isNotBlank(originalLink)) {
((HttpServletResponse) response).sendRedirect(originalLink);
return;
}
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
.eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
if (shortLinkGotoDO == null) {
// 严谨来说此处需要进行封控
return;
}
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
.eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
.eq(ShortLinkDO::getDelFlag, 0)
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
if (shortLinkDO != null) {
stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl());
((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
}
} finally {
lock.unlock();
}
}
测试一下
{
"domain": "nurl.ink",
"originUrl": "https://nageoffer.com",
"gid": "1k91Uw",
"createdType": 1,
"validDateType": 1,
"validDate": "2025-06-01 00:00:00",
"describe": "唱山歌勒哎哎哎666"
}
我们首先创建一个短链接,得到nurl.ink/2128GK
记得hosts文件里一定要写127.0.0.1 nurl.ink
浏览器里输入nurl.ink:8001/2128GK
便可直接跳转nageoffer了
没有设置过期时间,不会发生缓存击穿
但是这种对 Redis 存储压力很大,很多短链接都是有周期性的,用完就不会再用了
总结一下,此乃双重判定锁:首先加分布式锁,防止缓存过期之后的大量请求过来的缓存穿透问题;同时再加上一个双重判定锁,可以让只有第一个拿到锁的请求进行缓存重构,之后拿到锁的请求直接查询缓存即可,提高了程序运行效率!
极端情况下 当我们的缓存没有原始链接(originalLink)大量的请求就回去访问数据库 去重构缓存,这时候我们要设置第一层锁 防止多个请求同时重建缓存(第一个if判断就涌入了很多请求 然后会有很多来获取这个锁 第一个获取锁的线程将数据重新添加到redis中 此时若是不加锁 其他线程就会重复获取锁 在执行这些查询操作 这些操作都是暴露数据库的操作 会造成数据库压力乃至崩溃) 当然为了节省数据库资源 在加一层锁 实现双重锁 第二层锁再次判定是否缓存 中包含原始链接 如果有 则直接去缓存 如果没有就去数据库里面查询在把数据库中的 数据传入给缓存
代码里面根本没有加两层分布式锁的概念,只是多了再次判断缓存是否有数据的代码! 我说一下我的思路:首先大量请求查询失效的key,第一个拿到锁的请求会先查询缓存,如果缓存里没有数据(第一重判断),则数据库调数据+重构缓存,重构完毕后释放锁;紧跟着其余的请求继续拿到锁,此时补加的缓存判断(第二重判断)才是双重判定锁的精髓,因为之前缓存已被重构,因此其余的请求,会直接在缓存中查到数据,不会进入if循环,直接返回。 换一种更合适的说法应该是分布式锁+双重缓存判定,跟ConcurrentHashMap中的ensuresegment的创建 segment对象 源码一样的思路
- 第17节:短链接跳转原始链接功能(缓存穿透)
首先第一步就是在这里,然后如果原始链接等于空的话,就先不获取分布式锁,我们先 判断 boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);如果不存在直接return好吧,接下来看是否是空值
RedisKeyConstant添加这个
/** * 短链接空值跳转前缀 Key */ public static final String GOTO_IS_NULL_SHORT_LINK_KEY = "short-link_is-null_goto_%s";
如果是blank的话,我们就也是一样的 return
if (StrUtil.isNotBlank(gotoIsNullShortLink)) { return; }
接下来是分布式锁,锁住它之后去获取,如果获取之后那这里的话如果说它等于空设置一个“-”
完整的函数如下:
@SneakyThrows @Override public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) { String serverName = request.getServerName(); String fullShortUrl = serverName + "/" + shortUri; String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl)); if (StrUtil.isNotBlank(originalLink)) { ((HttpServletResponse) response).sendRedirect(originalLink); return; } boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl); if (!contains) { return; } String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl)); if (StrUtil.isNotBlank(gotoIsNullShortLink)) { return; } RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl)); lock.lock(); try { originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl)); if (StrUtil.isNotBlank(originalLink)) { ((HttpServletResponse) response).sendRedirect(originalLink); return; } LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl); ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper); if (shortLinkGotoDO == null) { stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES); return; } LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid()) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0); ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper); if (shortLinkDO != null) { stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()); ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl()); } } finally { lock.unlock(); } }
这里有个布隆过滤器的逻辑存在问题,后边想了一个比较合适的方案,已在后面视频重构,大家往后看即可。
Be outlier:不理解为啥先查缓存再查布隆过滤器。不应该反过来吗
2023-11-28 19:00
start 回复 Be outlier:我的理解是布隆过滤器是判断当前短链接在数据库是否存在,而不是判断是否在缓存中存在。就算你先判断布隆过滤器,得出当前短链接是存在的,然后去查缓存,缓存中也可能不存在,还是得去查数据库
2023-12-10 20:24
wdnmd 回复 start:我觉得还是先查布隆,布隆中没有,那么能直接说明数据库没有,没有是不会误判的,这里就能排除一大部分无效请求,如果说布隆中有,那么缓存中不一定有,所以去查一下缓存,缓存中有的话,那么直接返回,缓存中没有的话,再判定key是否是空值,如果是空key,缓存中肯定没有,直接返回。查询到这里就结束了。后面的步骤都不需要,因为这一定是在查询数据库没有的数据,肯定不让查
2023-12-18 19:47
wdnmd 回复 start:不对,空key,说明恰好这一瞬间可能key失效了,时间到了。那么还得是回盘,去查,然后只让一个线程过去查。再更新缓存,将数据添加到布隆,没错 大概就是这样
2023-12-18 19:50
王小明 回复 wdnmd:我倒觉得查布隆过滤器和查redis的的顺序无所谓,都可以完成业务逻辑,只不过用户大多数情况下,还是会访问存在的短链接,先查redis可能速度快一点,因为先查布隆过滤器,也是需要查询redis的
2024-01-03 17:06
Mirac 回复 Be outlier:去看大话面试专题,都是先查缓存再查过滤器
2024-01-08 17:07
nnnn 回复 Be outlier:我的理解是:如果缓存中有数据,就直接返回了,要是没有的话才去查库,查库前用布隆过滤器判断一下是不是恶意数据。所以这个过程中,缓存是必查的,布隆过滤器不一定查。所以要是先查布隆过滤器,它就变成必查的了,就不太合逻辑。。
2024-01-26 22:40
暂无爱人 回复 Be outlier:感觉马哥对布隆过滤器的误判弄错了吧?返回结果为true,元素可能不在bloom里,返回结果为false,一定不在bloom里。
2024-02-18 11:45
202 回复 Be outlier:我感觉是可以减少误判(误判:缓存有原始链接对应的key,布隆过滤器误以为没有直接return了)
2024-04-10 01:22
资深老萌新 回复 202:布隆说没有一定没有,这个不会误判的
2024-04-23 14:37
请叫我初学者 回复 Mirac:大话面试专题链接有没
2024-08-11 18:02
皮蛋瘦肉粥。 回复 Be outlier:先缓存再布隆:缓存查到就一定查到,查不到就一定查不到,查到了就直接返回。先布隆后缓存:布隆查到不一定真存在,得再去缓存查一次。因为会误判存在的情况。这里可能就有疑问了。既然这样直接查询缓存行了,为什么还要再去布隆查?因为ttl可能过期了。
2024-11-02 19:35
。 回复 wdnmd:你在说什么啊?
2024-12-30 12:24
。 回复 Be outlier:我是觉得平常肯定是,存在于数据库的短链接请求远远大于恶意请求,不要本末倒置
Monou:第2步查询缓存未命中后,第三步查询布隆不存在后,为什么还要查询一次缓存??避免高并发的时候已经有其他请求查询过数据库缓存空对象了??
2023-12-13 21:33
旺旺掀被 回复 Monou:第一,这个是考虑缓存击穿,若是数据库已经确定这个Key不存在,则会将这个Key缓存一个空值。如果不加以判断,又会将请求打到数据库,等于对这个Key缓存的空值没有派上用场。 第二,布隆过滤器对值存在的情况下仍然可能误判,所以需要再查一次,避免许多误判的请求打到数据库。 个人浅见哈
2023-12-18 20:56
王小明 回复 Monou:布隆过滤器不存在不是直接返回了吗?
2024-01-03 17:07
昨日 回复 旺旺掀被:这个不是缓存穿透吗
缓存穿透还能用锁吗,不是大量无效的请求吗?这些请求重复的概率也会很大吧?缓存穿透还能用锁吗,不是大量无效的请求吗?这些请求重复的概率也会很大吧
布隆过滤器存在可能会误判,不存在就一定不存在
布隆过滤器返回存在结果,真实情况可能存在也可能不存在;返回不存在,则一定不存在
有没有这种情况:如果有大量并发请求尝试访问一个不存在的短链接,每个请求都会尝试获取相同的分布式锁。一旦获取锁的请求完成执行,它会在Redis中设置一个表示短链接不存在的空值。然而,由于锁的序列化访问特性,后续获得锁的请求仍会执行相同的数据库查询逻辑,查询数据库确认短链接确实不存在,然后再次在Redis中设置相同的空值。
如果不加布隆过滤器,如果有恶意请求,Redis 很快就会被放空值填满的
可以理解为布隆过滤器和缓存空值这两种方式搭配起来去解决缓存穿透这个问题吗?解决布隆过滤器误判的问题
可以的,还要加上分布式锁
Ecstasia:@马丁 如果大量并发请求尝试访问一个不存在的短链接,且刚好被布隆过滤器误判为数据库存在,且这个短链接还没有被缓存null值,那么这些请求去抢占锁,第一个拿到锁的线程会查库并重构此链接的空缓存,但是后面拿到锁的线程会重复执行第一个线程的代码,这边获取锁后是不是需要再加一个二次判定空值的逻辑。
2024-05-31 15:56
马丁 回复 Ecstasia:很好的问题,这个也有其他同学提到过,最新的代码已经加上了
2024-05-31 15:57
1
白日梦想家 回复 Ecstasia:后面拿到锁的线程不是会直接缓存命中吗?为啥会重复执行第一个线程的代码
2024-11-21 22:34
白日梦想家 回复 Ecstasia:昨天晚上想了好久,终于理解这个代码为啥会重复执行了
大量并发请求访问一个不存在的短链接,并且正好它们都走到了抢占锁这一步时,虽然第一个拿到锁的线程会进行缓存重构。但是由于数据库中根部不存在它们访问的数据,导致这里数据库查询失败没有重构缓存,因此后续的线程拿到锁之后,查询redis仍未命中,会继续重复第一个线程的代码。所以这里拿到锁之后,应该还要去查一下空缓存避免重复查询的情况
白日梦想家:自己画了一张这节的流程图,这里在获取到分布式锁之后应该还要二次查询一次空缓存,否则后续拿到锁的线程可能会重复执行第一个拿到锁的线程的代码。希望大佬指正。
2024-11-22 09:36
花开富贵🌸 回复 白日梦想家:第二列“查询缓存是否存在空缓存”的逻辑判断存在问题,按照目前代码理解应该是不存在return,存在则往下执行流程,且下面为空处理存放的是“-”。当第一次为空值请求的时候,添加到缓存中且值为“-”,后续相同恶意请求时判断不为空则拦截直接返回。
2024-11-25 11:33
白日梦想家 回复 花开富贵🌸:空缓存不是为了防止缓存穿透才设置的吗,为啥存在空缓存了还要继续往下执行?
2024-11-25 21:24
花开富贵🌸 回复 白日梦想家:当第一次为空值请求的时候,添加到缓存中且值为“-”,后续相同恶意请求时判断不为空则拦截直接返回。 这个地方的“空缓存”并不是真的空,在代码中的核心逻辑是缓存的数据是横杠:“-”
2024-12-10 17:49
白日梦想家 回复 花开富贵🌸:可能是我表述不太好吧,我说的空缓存就是指的-这个缓存。这个缓存存在的时候(也就是我说的空缓存存在),直接返回。否则,才进行后续逻辑处理。跟你是一个逻辑
2024-12-11 08:08
白日梦想家 回复 花开富贵🌸:表述应该改成“如果查询到空缓存中有值,说明可能发生缓存穿透,直接返回。否则,进行后续的处理逻辑”
2024-12-11 08:22
pyxxx:判断请求key是否存在空值,存在直接返回空,为空则请求数据库这里没听懂。请求key为什么会为空
2025-01-25 14:07
lovqq 回复 pyxxx:某个短链接redis没有数据库没有,并且布隆过滤器误判了,这时在查询goto表会查不到然后缓存空对象,判断是否存在空值就是看下数据库中到底有没有,有的话就是误判了
2025-02-11 21:42
Superzl. 回复 pyxxx:他这里表述确实容易让人迷糊,这里“存在空值”指的是查询数据库发现没有之后往缓存里存的那个"-",不存在这个“-”就去查数据库
- 第18节:短链接跳转原始链接功能(缓存预热)
关于缓存预热这一块,那就是如果说我们创建了一批对吧短链接,然后因为我们创建好之后,其实它只是在数据库里面没在缓存里面,然后这个时候一大批流量来访问这些短链接,那么其实就会造成一些比如说阻塞以及一些其他的事情,反正他肯定是没有说直接创建出来就放到短链接的这个缓存里面对吧?肯定是不用这种的,这样的话就会涉及到一个概念就是缓存预热,缓存预热什么意思呢?就相当于把一些我们已知的这种访问量,就是已知必会被访问的数据对吧?先给它放到缓存里面,避免再通过这种运行时被用户访问在加载的环境的这种行为好吧?然后我们这里的话是通过你创建好短链接之后,我们就直接去往缓存里面去放,然后这边的话我们通过一个叫做什么我想一下,然后我们要在这里去给他点ops,value,点set,然后K的话应该是我看一下。K的话应该是 four shut URL。然后value就是我们的什么?就是我们的跳转路径对不对?应该是通过快速拍上点原始,ok,但是有一点就是什么?我们的短链接分为两种,一种是永久有效期,一种是临时有效,对吧?这样的话我们就需要做一件事情,就是要给他设置有效期。Ok我们这边创建一个创建一个类,然后link游艇,我们短链接工具类,然后一个public答题课,然后的返回一个有效期时间get catch。给它定个catch,然后有效data。然后这边的话我们以一个日期当做一个午餐,因为我们是什么?因为我们它如果说是永久有效,它的有效期肯定空,如果说是临时有效,有效期就会有值,我们这边就直接判断一下,而不是第二我也对,然后第二如果说它不为空,那么我们干什么就给大家做一个对他要求 be to be BT wwenen. 然后一个当前时间,然后去干什么?去向后去给它做一个什么?做一个做一个后移,然后这里给你,然后我们在这里继续第二如果说,它等于空,那就证明一件事情,那就证明什么?那就证明这个是永久有效,永久有效的话我们要给它设置一个默认值,这个默认值的话,我们在这里面给它创建一个创建一个长项链。 Link. 就说个L。Shut up you are. Shut the link. Shut the link. What's up? Stant. Okay. 短链接长两类,然后叫什么帕布里克,然后一个斯塔迪克final浪,然后是一个叫做default。刚谁开始,然后一个超时不对,应该叫过期的。过期,然后一个var D的时间,然后过期的话我们就默认给他什么,我们就默认给他做一个默认一个月,一个月等于多少?毫秒。我操去了。对不对?永久短链接默认缓存有效时间,半句已经。然后我带大家看一下这个方法,如果说我们调查这个方法就相当于是获取短期的有效期的,如果有效期如果说,用户创建的是一个带有效期的短链接,那么这就是它对应的有效期如果说是永久的,那么这个就是个如果说,它是带有效期的话,我们就拿这个时间和当前时间去比,获得中间的一个差值以毫秒计算。然后如果说它这个值等于空,就我们走X逻辑就复制一个默认的有效期,就相当于如果是永久短链接,那么它的缓存的有效时间就是一个月,获取短链接的有效期时间。对,不太方便觉得太慢。有效期时间。然后有效期时间错了。这样难搞,对,然后立刻另外的话就吃了,就这样子的话看到怎么说,然后这里的话我们直接取个快速的给他,然后一个探ok。然后这边的话就相当于我们做了一个什么?做了一层缓存预热,然后这边的话我们介绍一下给它考虑给它拆分开,这样喜欢稍微美观一点。
package com.nageoffer.shortlink.project.common.constant; /** * 短链接常量类 */ public class ShortLinkConstant { /** * 永久短链接默认缓存有效时间 */ public static final long DEFAULT_CACHE_VALID_TIME = 2626560000L; }
package com.nageoffer.shortlink.project.toolkit; import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; import java.util.Date; import java.util.Optional; import static com.nageoffer.shortlink.project.common.constant.ShortLinkConstant.DEFAULT_CACHE_VALID_TIME; /** * 短链接工具类 */ public class LinkUtil { /** * 获取短链接缓存有效期时间 * * @param validDate 有效期时间 * @return 有限期时间戳 */ public static long getLinkCacheValidTime(Date validDate) { return Optional.ofNullable(validDate) .map(each -> DateUtil.between(new Date(), each, DateUnit.MS)) .orElse(DEFAULT_CACHE_VALID_TIME); } }
@Override public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) { String shortLinkSuffix = generateSuffix(requestParam); //ShortLinkDO shortLinkDO = BeanUtil.toBean(requestParam, ShortLinkDO.class); //shortLinkDO.setShortUri(shortLinkSuffix); //String fullShortUrl = requestParam.getDomain() + "/" + shortLinkSuffix; String fullShortUrl = StrBuilder.create(requestParam.getDomain()) .append("/") .append(shortLinkSuffix) .toString(); //shortLinkDO.setEnableStatus(0); //shortLinkDO.setFullShortUrl(requestParam.getDomain() + "/" + shortLinkSuffix); ShortLinkDO shortLinkDO = ShortLinkDO.builder() .domain(requestParam.getDomain()) .originUrl(requestParam.getOriginUrl()) .gid(requestParam.getGid()) .createdType(requestParam.getCreatedType()) .validDateType(requestParam.getValidDateType()) .validDate(requestParam.getValidDate()) .describe(requestParam.getDescribe()) .shortUri(shortLinkSuffix) .enableStatus(0) .fullShortUrl(fullShortUrl) .build(); ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder() .fullShortUrl(fullShortUrl) .gid(requestParam.getGid()) .build(); try { baseMapper.insert(shortLinkDO); shortLinkGotoMapper.insert(linkGotoDO); } catch (DuplicateKeyException ex) { LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl); ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper); if (hasShortLinkDO != null) { log.warn("短链接:{} 重复入库", fullShortUrl); throw new ServiceException("短链接生成重复"); } } stringRedisTemplate.opsForValue().set( fullShortUrl, requestParam.getOriginUrl(), LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS ); shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);//原为shortLinkSuffix return ShortLinkCreateRespDTO.builder() //.fullShortUrl(shortLinkDO.getFullShortUrl()) .fullShortUrl("http://" + shortLinkDO.getFullShortUrl()) .gid(requestParam.getGid()) .originUrl(requestParam.getOriginUrl()) .build(); }
创建短链接的时候设置Redis过期时间与短链接过期时间一致,那怎么还会有缓存击穿?除了永久有效的短链接可能会存在Redis过期了但短链接未过期,其他短链接基本都是Redis过期了,短链接也跟着过期了。 如果有很多短链接创建之后基本没有使用,那设置这么长的过期时间是不是会对对Redis造成太大的存储压力?
短链接长时间的存储对 Redis 压力很大,很多短链接都是有周期性的,用完就不会再用了
一股脑全扔redis里面吗?不需要用什么方法判断热点短链接啥的吗
一般来说,这个阶段不存在热 Key,可以看看京东 HotKey 开源框架
缓存过期时间工具类: 1. 如果date超过当前时间一周,那么设置过期时间为一周,并随机一个0~24小时的时间 2. 如果date没有超过当前时间一周,那么设置指定的时间 3. 如果date为null,则设置过期时间为一周,并随机一个0~24小时的时间 各路大神检查一下是否可行 public static final long ONE_WEEK_MILLIS = 7 * 24 * 60 * 60 * 1000L; /** * 获取短链接缓存过期时间戳 * * @param date 日期 * @return 短链接缓存过期时间戳 */ public static long getShortLinkCacheTime(Date date) { // 当前时间 long currentTimeMillis = System.currentTimeMillis(); // 如果 date 为 null,则默认设置为一周后的时间戳,并随机一个 0 ~ 24 小时的时间 if (date == null) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, 7); return calendar.getTimeInMillis() + ThreadLocalRandom.current().nextLong(0, 24 * 60 * 60 * 1000L); } long dateMillis = date.getTime(); // 检查 date 是否超过一周 if (dateMillis - currentTimeMillis > ONE_WEEK_MILLIS) { // 如果超过一周,那么将有效时间设置在一周后 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, 7); return calendar.getTimeInMillis() + ThreadLocalRandom.current().nextLong(0, 24 * 60 * 60 * 1000L); } else { // 如果没有超过一周,则设置为 date 的时间戳 return dateMillis; } }
缓存预热的key应该是String.format(GOTO_SHORT_LINK_KEY, fullShortUrl)这个,不然的话,短连接跳转的时候还是取不到value
- 第19节:短链接跳转原始链接功能(区分过期短链)
永久短链接默认缓存有效时间,默认一个月
shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);//原为shortLinkSuffix return ShortLinkCreateRespDTO.builder() //.fullShortUrl(shortLinkDO.getFullShortUrl()) .fullShortUrl("http://" + shortLinkDO.getFullShortUrl()) .gid(requestParam.getGid()) .originUrl(requestParam.getOriginUrl()) .build();
如果说短链接过期,因为我们短链接是有有效期的嘛,我的短链期有效期是2022年这样sql查询仍然可以查出来,因为他这里面干什么是没加有效期的
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid()) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0);
那所以说我们在这里面就得如果说查出来的数据不等于空,shortLinkDO.getValidDate() != null,相当于是那个临时有效。并且就是在当前这个时间之前,它这个短连接已经相当于过期了,那过期的话就跟无是一样的道理
if (shortLinkDO != null) { //stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()); if (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date())) { stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES); return; } stringRedisTemplate.opsForValue().set( String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl(), LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS ); ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl()); } } finally { lock.unlock(); }
feature:短链接跳转时如果已过有效期设置为空
hutool的between方法里面计算时间差是带绝对值的,有个重载方法isAbs置为false。更严谨一点;一般来说,如果这个差值没那么离谱,是能接受的
如果为啥要设计两个有效期呢?是为了留痕吗?因为mysql是软删除,所以这个过期时间需要持久化在数据库中?创建短链接时有过期时间,对应到数据库。缓存的过期时间是为了避免不再使用的短链接长时间占用内存
短链接过期了就不需要再在redis设置了,因为到期不是自动消失了吗?咱们现在就是如果过期不再设置,似乎没问题;应该是防止缓存穿透吧。如果有大量并发的请求访问已过期的短链接,不设置空缓存的话,可能会对数据库造成巨大压力。
有效期过了redis里面不就自动清除了吗?为什么还要多此一举;我觉得这里主要是防止后面的人通过原始短链接去不断访问这个失效的,因为缓存中没有了嘛,就要去访问数据库,结果数据库没有设置查询过期条件,这样就把短链接又存到缓存中了,所以不如直接设置一个空值缓存,让用户访问不到
天才,我刚开始没明白怎么设置为“永久有效”的有效期怎么比“非永久有效”的时间还少,才一个月,直到shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date())这个判断,这样设置为永久有效的就会永远会被存储到缓存中,等于说是一个月后缓存中就消失了,但是如果再有请求来访问,那么这个又会被放进缓存中,这样就相当于永久有效了,而且还不是一直占用内存,真是天才思路!!!
- 第20节:短链接不存在跳转指定页面功能
短链接跳转,如果说短链接不存在,我们应该返回什么样的一个页面。现在你直接访问一个不存在的短链接是直接返回空白页面,发现掘金在这方面不错
在project的pom引入 thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
配置文件application里面
spring:
mvc:
view:
prefix: /templates/
suffix: .html
不存在页面notfound.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,
maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover"
/>
<link rel="shortcut icon" href="" />
<meta name="theme-color" content="#000000" />
<meta property="og:title" lang="zh-CN" content="" />
<meta name="theme-color" content="#000000" />
<meta property="og:type" content="video" />
<meta property="og:title" content="" />
<meta property="og:description" content="" />
<meta property="og:image" content="" />
<meta property="og:image:width" content="750" />
<meta property="og:image:height" content="1334" />
<title></title>
<style>
.container,
.pc-container {
margin-top: 32vh;
background: white;
display: flex;
align-items: center;
flex-direction: column;
}
.text {
color: #333333;
line-height: 28px;
}
.container .text {
margin-top: 16px;
font-size: 3vw;
}
.pc-container .text {
/* margin-top: 100px; */
font-size: 18px;
}
.pc-container .img {
height: 200px;
}
.container .img {
width: 50vw;
}
textarea {
width: 90vw;
}
</style>
</head>
<body>
<div class="pc-container">
<div>
<img
class="img"
src="//p3-live.byteimg.com/tos-cn-i-gjr78lqtd0/c03071dcdc52c24e0aab256518e51557.png~tplv-gjr78lqtd0-image.image"
/>
</div>
<div class="text">您访问的页面不存在,请确认链接是否正确</div>
</div>
</body>
</html>
重定向接下来,在controller层里面重新定义控制器
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 短链接不存在跳转控制器
*/
@Controller
public class ShortLinkNotFoundController {
/**
* 短链接不存在跳转页面
*/
@RequestMapping("/page/notfound")
public String notfound() {
return "notfound";
}
}
定义成简单的视图
首先如果不论过滤器不存在,那短链接肯定不存在,直接给他进行重定项;gotoIsNullShortLink列表里面,那一样要给他去做返回;gotoDO一样要去给它做跳转;再然后如果说这个已经过了有效期,一样要进行一个跳转:部分代码如下
boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl); if (!contains) { ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; } String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl)); if (StrUtil.isNotBlank(gotoIsNullShortLink)) { ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; } RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl)); lock.lock(); try { originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl)); if (StrUtil.isNotBlank(originalLink)) { ((HttpServletResponse) response).sendRedirect(originalLink); return; } LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class) .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl); ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper); if (shortLinkGotoDO == null) { stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES); ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; } LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class) .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid()) .eq(ShortLinkDO::getFullShortUrl, fullShortUrl) .eq(ShortLinkDO::getDelFlag, 0) .eq(ShortLinkDO::getEnableStatus, 0); ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper); if (shortLinkDO != null) { //stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl()); if (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date())) { stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES); ((HttpServletResponse) response).sendRedirect("/page/notfound"); return; }
refactor:重构短链接不存在跳转提示页面
@RestController和@Controller的区别: 被@Controller标识的类中的方法返回值会经过SpringMVC中的视图解析器解析并渲染,最终将生成的 HTML 等格式的页面返回给客户端。而被@RestController标识的类中的方法会默认将返回值直接写入http请求的响应体中而不是返回视图名称。 举个例子:如果某个方法返回字符串"hello", 如果该方法存在于被@Controller标识的类中,那么会返回一个名为hello的视图。这个视图可能是一个 Thymeleaf 模板文件(hello.html)或者 JSP 文件等,具体由视图解析器的配置决定。 如果这个方法存在于被@RestController标识的类中,那么会直接将返回值写入http的响应体中,返回给前端。 跳转失败的记得刷一下maven依赖
- 第21节:获取目标网站标题功能
来做一个小功能就是通过URL来获取当前网站的一个标题。首先我们先看一下短链接,然后它这个里面的话我们可以看到这个,我们在这里面输入短信之后,它是能够拿到这个标题的,然后我们看一下它这里的话,其实也是发起一个链接,一个什么也是发起一个APP调的是他们的后端,我们看一下配置抬头对吧?因为这个东西叫配置title,也是发起了一个东西叫把原始链接给他传过去了,然后干什么?他这里面返回了一个什么返回了一个开头对吧?就把这个返回了,然后这种应该怎么实现对吧?我跟我们前端去聊一下,他说可能会存在前端跨越的一个问题,然后尽量需要后端来做对吧?然后我们共享他的PPT,他给我们推荐了一个内裤,推荐了一个这玩意我们可以试一下,我们可以尝试做一下;首先肯定是要在我想想,这个是要在我的命里面我们需要在里面去做,我想一下需不需要在革命里面?不行还是得在这个里面,因为可能别人也会去传需要一个接口中台化的对外暴露的接口,人家也需要我们就先定义在我的admin里面,不是先定义在这块价格里面。
project的UrlTitleController,老规矩记得注入
package com.nageoffer.shortlink.project.controller; import com.nageoffer.shortlink.project.common.convention.result.Result; import com.nageoffer.shortlink.project.common.convention.result.Results; import com.nageoffer.shortlink.project.service.UrlTitleService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * URL 标题控制层 */ @RestController @RequiredArgsConstructor public class UrlTitleController { private final UrlTitleService urlTitleService; /** * 根据 URL 获取对应网站的标题 */ @GetMapping("/api/short-link/v1/title") public Result<String> getTitleByUrl(@RequestParam("url") String url) { return Results.success(urlTitleService.getTitleByUrl(url)); } }
UrlTitleService自己加一下,还有pom中加一个依赖<jsoup.version>1.15.3</jsoup.version>,具体到项目中是这样的
<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>${jsoup.version}</version> </dependency>
package com.nageoffer.shortlink.project.service; /** * URL 标题接口层 */ public interface UrlTitleService { /** * 根据 URL 获取标题 * * @param url 目标网站地址 * @return 网站标题 */ String getTitleByUrl(String url); }
UrlTitleController是admin里面的,admin也有做一下透传
package com.nageoffer.shortlink.admin.controller; import com.nageoffer.shortlink.admin.common.convention.result.Result; import com.nageoffer.shortlink.admin.remote.ShortLinkRemoteService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * URL 标题控制层 */ @RestController @RequiredArgsConstructor public class UrlTitleController { /** * 后续重构为 SpringCloud Feign 调用 */ ShortLinkRemoteService shortLinkRemoteService = new ShortLinkRemoteService() { }; /** * 根据URL获取对应网站的标题 */ @GetMapping("/api/short-link/admin/v1/title") public Result<String> getTitleByUrl(@RequestParam("url") String url) { return shortLinkRemoteService.getTitleByUrl(url); } }
UrlTitleServiceImpl
package com.nageoffer.shortlink.project.service.impl; import com.nageoffer.shortlink.project.service.UrlTitleService; import lombok.SneakyThrows; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.springframework.stereotype.Service; import java.net.HttpURLConnection; import java.net.URL; /** * URL 标题接口实现层 */ @Service public class UrlTitleServiceImpl implements UrlTitleService { @SneakyThrows @Override public String getTitleByUrl(String url) { URL targetUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection(); connection.setRequestMethod("GET"); connection.connect(); int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { Document document = Jsoup.connect(url).get(); return document.title(); } return "Error while fetching title."; } }
ShortLinkRemoteService.java添加
/** * 根据 URL 获取标题 * * @param url 目标网站地址 * @return 网站标题 */ default Result<String> getTitleByUrl(@RequestParam("url") String url) { String resultStr = HttpUtil.get("http://127.0.0.1:8001/api/short-link/v1/title?url=" + url); return JSON.parseObject(resultStr, new TypeReference<>() { }); }
feature:开发获取短链接目标网站标题接口
要做个超时的,要不然像github就卡哪里了
这个方法放在admin还是中台里,有什么说法吗?因为 project 会通过 api 访问,所以只能放在 project 里
- 第22节:获取目标网站图标功能
@SneakyThrows private String getFavicon(String url) { URL targetUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection(); connection.setRequestMethod("GET"); connection.connect(); int responseCode = connection.getResponseCode(); if (HttpURLConnection.HTTP_OK == responseCode) { Document document = Jsoup.connect(url).get(); Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first(); if (faviconLink != null) { return faviconLink.attr("abs:href"); } } return null; }
updateShortLink的builder()和builder里面添加一句
.favicon(getFavicon(requestParam.getOriginUrl()))
{
"domain": "nurl.ink",
"originUrl": "https://nageoffer.com",
"gid": "1k91Uw",
"createdType": 1,
"validDateType": 0,
"describe": "唱山歌勒哎哎哎666"
}
创建短链接,得到nurl.ink/VuI6a
在t_link_goto_15,t_link_4,里面我们看看数据库
feature:开发短链接创建时获取原始网站图标
有位同学遇到了跳转问题,这里补充下,希望能帮助需要的同学
短链接关于获取目标网站图标功能,今天在跟着马哥视频一步一步敲的时候,发现获取不到目标网站的图标,调试时发现是respondCode等于301,表示永久重定向,说明请求的资源已经不存在了,需改用新的URL再次访问。于是询问chatGPT,解决了这个问题,希望可以帮助到跟我一样遇到这个问题的朋友们
Comments NOTHING