8:修改短链接
前端传入的数据 - ShortLinkUpdateReqDTO
/**
* 短链接修改请求对象
*/
@Data
public class ShortLinkUpdateReqDTO {
/**
* 原始链接
*/
private String originUrl;
/**
* 完整短链接
*/
private String fullShortUrl;
/**
* 原始分组标识
*/
private String originGid;
/**
* 分组标识
*/
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;
}
controller
/**
* 修改短链接
*/
@PostMapping("/api/short-link/v1/update")
public Result<Void> updateShortLink(@RequestBody ShortLinkUpdateReqDTO requestParam) {
shortLinkService.updateShortLink(requestParam);
return Results.success();
}
ShortLinkServiceImpl
@Transactional(rollbackFor = Exception.class)
@Override
public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
verificationWhitelist(requestParam.getOriginUrl());
LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
.eq(ShortLinkDO::getGid, requestParam.getOriginGid())
.eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
.eq(ShortLinkDO::getDelFlag, 0)
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);//根据当前完整短链接以及原始gid查询数据库
if (hasShortLinkDO == null) { //如果没有查到,说明短链接被删除了,或者是说传进来的参数有误
throw new ClientException("短链接记录不存在");
}
if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) { //传入的gid与想要修改的gid是否一致,如果一致,说明没要求对当前的短链接进行换组
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); //检查一下是否设置为永久有效,如果是永久有效的化,就将其设置为null,如果不是的化,就设置为想要设置的有效时间
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.domain(hasShortLinkDO.getDomain())
.shortUri(hasShortLinkDO.getShortUri())
.favicon(hasShortLinkDO.getFavicon())
.createdType(hasShortLinkDO.getCreatedType())
.gid(requestParam.getGid())
.originUrl(requestParam.getOriginUrl())
.describe(requestParam.getDescribe())
.validDateType(requestParam.getValidDateType())
.validDate(requestParam.getValidDate())
.build();
baseMapper.update(shortLinkDO, updateWrapper); //对要更新的内容进行更新
} else {
// 为什么监控表要加上Gid?不加的话是否就不存在读写锁?详情查看:https://nageoffer.com/shortlink/question
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl())); //创建一个读写锁对象,锁键为:LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()
RLock wLock = readWriteLock.writeLock();//写锁对象
wLock.lock();//尝试获取写锁,如果写锁被其他线程正在使用着,那么当前线程会进入阻塞状态,知道锁释放
try {
LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
.eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
.eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
.eq(ShortLinkDO::getDelFlag, 0)//未被删除
.eq(ShortLinkDO::getDelTime, 0L)//表示删除时间为0
.eq(ShortLinkDO::getEnableStatus, 0);
ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
.delTime(System.currentTimeMillis())
.build();
delShortLinkDO.setDelFlag(1);
baseMapper.update(delShortLinkDO, linkUpdateWrapper); //在 GID 发生变化时,先将旧的短链接记录标记为删除(逻辑删除),以便后续插入新的记录。
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.domain(createShortLinkDefaultDomain)
.originUrl(requestParam.getOriginUrl())
.gid(requestParam.getGid())
.createdType(hasShortLinkDO.getCreatedType())
.validDateType(requestParam.getValidDateType())
.validDate(requestParam.getValidDate())
.describe(requestParam.getDescribe())
.shortUri(hasShortLinkDO.getShortUri())
.enableStatus(hasShortLinkDO.getEnableStatus())
.totalPv(hasShortLinkDO.getTotalPv())
.totalUv(hasShortLinkDO.getTotalUv())
.totalUip(hasShortLinkDO.getTotalUip())
.fullShortUrl(hasShortLinkDO.getFullShortUrl())
.favicon(getFavicon(requestParam.getOriginUrl()))
.delTime(0L)
.build();
baseMapper.insert(shortLinkDO);//插入新的gid
LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
.eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
.eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
shortLinkGotoMapper.delete(linkGotoQueryWrapper);
shortLinkGotoDO.setGid(requestParam.getGid());
shortLinkGotoMapper.insert(shortLinkGotoDO);
} finally {
wLock.unlock();
}
}
// 短链接如何保障缓存和数据库一致性?详情查看:https://nageoffer.com/shortlink/question
if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
|| !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())
|| !Objects.equals(hasShortLinkDO.getOriginUrl(), requestParam.getOriginUrl())) {
stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
Date currentDate = new Date();
if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(currentDate)) {
if (Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()) || requestParam.getValidDate().after(currentDate)) {
stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
}
}
}
}
主要的代码逻辑就是,当用户要修改短链接的时候,可能会将其分组也改了,所以这里就要加一层判断。
当分组没有改的时候,直接进行更新,然后看一下设置的短链接有效期时间是否是永久,如果是的化,就在数据库内设置为null
如果分组确实改了,那就先删除数据库中旧的短链接数据,然后添加新的分组短链接数据。
因为这里涉及到先删除后插入,所以使用分布式锁- 读写锁,确保 逻辑删除和插入操作的原子性,或脏读幻读
(1):并发插入问题:导致同一个短链接在数据库中不同的分组下出现多次
线程1:开始执行短链接更新操作。
- 标记旧的短链接记录为“已删除”。
- 尚未插入新记录。
线程2:同时开始处理相同的短链接更新操作。
- 线程2在线程1插入新记录之前,读取到了“已删除”的旧记录,并开始进行更新。因为此时线程1的事务还没有提交,getDelFlag = 0
- 最终,线程2可能也会插入一条新记录。
结果:数据库中可能会出现两条记录,一个是线程1插入的,另一个是线程2插入的。这会导致数据重复和不一致。
**解决措施:**加锁
(2):不完整事务问题
线程1:正在处理短链接更新操作。
- 标记旧记录为“已删除”。
- 尚未插入新记录。
线程2:在此时开始读取数据,读取到的是“已删除”的旧记录,并基于这个状态执行某些业务逻辑。
- 由于读取到了一个“不完整”的中间状态数据,线程2可能会执行一些不合适的操作,导致数据不一致或逻辑错误。例如展示当前用户有哪些短链接的时候,就会出现漏读的问题。
结果:业务逻辑可能被错误的数据状态所影响,造成进一步的问题。
**解决措施:**开启事务模式