冗余双写思路总结
新增全流程
Request
{
"groupId":"825487726962655232",
"title":"zzzzzz全链路测试",
"originalUrl":"https://ccl.zone?asdf=a21",
"domainId":"1613160674939047938",
"domainType":"CUSTOM",
"expired":"-1"
}
Controller
@PostMapping("/add")
public JsonData createShortLink(@RequestBody ShortLinkAddRequest request){
JsonData jsonData = shortLinkService.createShortLink(request);
return jsonData;
}
Service
Producer
@Override
public JsonData createShortLink(ShortLinkAddRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
String newOriginalUrl = CommonUtil.addUrlPrefix(request.getOriginalUrl());
request.setOriginalUrl(newOriginalUrl);
EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
.content(JsonUtil.obj2Json(request))
.messageId(IDUtil.geneSnowFlakeID().toString())
.eventMessageType(EventMessageType.SHORT_LINK_ADD.name())
.build();
rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(), rabbitMQConfig.getShortLinkAddRoutingKey(), eventMessage);
return JsonData.buildSuccess();
}
Consumer
Lua + Redis 释义
//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" + " elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" + " else return 0; end;";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);
* 先判断是否有,如没这个key,则设置key-value,配置过期时间,加锁成功
if redis.call('EXISTS',KEYS[1]*shortLinkCode*)==0 then redis.call('set',KEYS[1]*shortLinkCode*,ARGV[1]*accountNo*);
redis.call('expire',KEYS[1]*shortLinkCode*,ARGV[2]*100*);
* 如果有这个key,判断value是否是同个账号,是同个账号则返回加锁成功
elseif redis.call('get',KEYS[1])*shortLinkCode* == ARGV[1]*accountNo* then return 2
* 如果不是同个账号则加锁失败
else return 0; end;
/**
* 处理短链新增逻辑
* <p>
* //判断短链域名是否合法
* //判断组名是否合法
* //生成长链摘要
* //生成短链码
* //加锁
* //查询短链码是否存在
* //构建短链对象
* //保存数据库
*
* @param eventMessage
* @return
*/
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkAddRequest addRequest = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkAddRequest.class);
//短链域名校验
DomainDO domainDO = checkDomain(addRequest.getDomainType(), addRequest.getDomainId(), accountNo);
//校验组是否合法
LinkGroupDO linkGroupDO = checkLinkGroup(addRequest.getGroupId(), accountNo);
//长链摘要
String originalUrlDigest = CommonUtil.MD5(addRequest.getOriginalUrl());
//短链码重复标记
boolean duplicateCodeFlag = false;
//生成短链码
String shortLinkCode = shortLinkComponent.createShortLinkCode(addRequest.getOriginalUrl());
//加锁
//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
" elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +
" else return 0; end;";
Long result = redisTemplate.execute(new
DefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);
//加锁成功
if (result > 0) {
//C端处理
if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {
//先判断是否短链码被占用
ShortLinkDO shortLinCodeDOInDB = shortLinkManager.findByShortLinCode(shortLinkCode);
if (shortLinCodeDOInDB == null) {
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.accountNo(accountNo).code(shortLinkCode)
.title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl())
.domain(domainDO.getValue()).groupId(linkGroupDO.getId())
.expired(addRequest.getExpired()).sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
shortLinkManager.addShortLink(shortLinkDO);
return true;
} else {
log.error("C端短链码重复:{}", eventMessage);
duplicateCodeFlag = true;
}
} else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {
//B端处理
GroupCodeMappingDO groupCodeMappingDOInDB = groupCodeMappingManager.findByCodeAndGroupId(shortLinkCode, linkGroupDO.getId(), accountNo);
if (groupCodeMappingDOInDB == null) {
GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder()
.accountNo(accountNo).code(shortLinkCode).title(addRequest.getTitle())
.originalUrl(addRequest.getOriginalUrl())
.domain(domainDO.getValue()).groupId(linkGroupDO.getId())
.expired(addRequest.getExpired()).sign(originalUrlDigest)
.state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
groupCodeMappingManager.add(groupCodeMappingDO);
return true;
} else {
log.error("B端短链码重复:{}", eventMessage);
duplicateCodeFlag = true;
}
}
} else {
//加锁失败,自旋100毫秒,再调用; 失败的可能是短链码已经被占用,需要重新生成
log.error("加锁失败:{}", eventMessage);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
}
duplicateCodeFlag = true;
}
if (duplicateCodeFlag) {
String newOriginalUrl = CommonUtil.addUrlPrefixVersion(addRequest.getOriginalUrl());
addRequest.setOriginalUrl(newOriginalUrl);
eventMessage.setContent(JsonUtil.obj2Json(addRequest));
log.warn("短链码报错失败,重新生成:{}", eventMessage);
handlerAddShortLink(eventMessage);
}
return false;
}
Util
/**
* 校验域名
*
* @param domainType
* @param domainId
* @param accountNo
* @return
*/
private DomainDO checkDomain(String domainType, Long domainId, Long accountNo) {
DomainDO domainDO;
if (DomainTypeEnum.CUSTOM.name().equalsIgnoreCase(domainType)) {
domainDO = domainManager.findById(domainId, accountNo);
} else {
domainDO = domainManager.findByDomainTypeAndID(domainId, DomainTypeEnum.OFFICIAL);
}
Assert.notNull(domainDO, "短链域名不合法");
return domainDO;
}
/**
* URL增加前缀
* @param url
* @return
*/
public static String addUrlPrefix(String url){
return IDUtil.geneSnowFlakeID()+"&"+url;
}
/**
* 生成短链码
* @param param
* @return
*/
public String createShortLinkCode(String param){
long murmurhash = CommonUtil.murmurHash32(param);
//进制转换
String code = encodeToBase62(murmurhash);
String shortLinkCode = ShardingDBConfig.getRandomDBPrefix(code) + code + ShardingTableConfig.getRandomTableSuffix(code);
return shortLinkCode;
}
/**
* 校验组名
*
* @param groupId
* @param accountNo
* @return
*/
private LinkGroupDO checkLinkGroup(Long groupId, Long accountNo) {
LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
Assert.notNull(linkGroupDO, "组名不合法");
return linkGroupDO;
}
/**
* MD5加密
*
* @param data
* @return
*/
public static String MD5(String data) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
} catch (Exception exception) {
}
return null;
}
RabbitMQ
Listener
Link
@Component
@Slf4j
//@RabbitListener(queues = "short_link.add.link.queue")
@RabbitListener(queuesToDeclare = {@Queue("short_link.add.link.queue")})
public class ShortLinkAddLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddLinkMQListener message消息内容:{}", message);
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_LINK.name());
shortLinkService.handlerAddShortLink(eventMessage);
} catch (Exception e) {
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
//确认消息消费成功
// channel.basicAck(tag, false);
}
}
Mapping
@Component
@Slf4j
//@RabbitListener(queues = "short_link.add.mapping.queue")
@RabbitListener(queuesToDeclare = {@Queue("short_link.add.mapping.queue")})
public class ShortLinkAddMappingMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) throws IOException {
log.info("监听到消息ShortLinkAddMappingMQListener message消息内容:{}", message);
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_ADD_MAPPING.name());
shortLinkService.handlerAddShortLink(eventMessage);
} catch (Exception e) {
//处理业务异常,还有进行其他操作,比如记录失败原因
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
//确认消息消费成功
// channel.basicAck(tag, false);
}
}
Config
/**
* 交换机
*/
private String shortLinkEventExchange="short_link.event.exchange";
/**
* 创建交换机 Topic类型
* 一般一个微服务一个交换机
* @return
*/
@Bean
public Exchange shortLinkEventExchange(){
return new TopicExchange(shortLinkEventExchange,true,false);
}
//新增短链相关配置====================================
/**
* 新增短链 队列
*/
private String shortLinkAddLinkQueue="short_link.add.link.queue";
/**
* 新增短链映射 队列
*/
private String shortLinkAddMappingQueue="short_link.add.mapping.queue";
/**
* 新增短链具体的routingKey,【发送消息使用】
*/
private String shortLinkAddRoutingKey="short_link.add.link.mapping.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
*/
private String shortLinkAddLinkBindingKey="short_link.add.link.*.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
*/
private String shortLinkAddMappingBindingKey="short_link.add.*.mapping.routing.key";
/**
* 新增短链api队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkAddApiBinding(){
return new Binding(shortLinkAddLinkQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddLinkBindingKey,null);
}
/**
* 新增短链mapping队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkAddMappingBinding(){
return new Binding(shortLinkAddMappingQueue,Binding.DestinationType.QUEUE, shortLinkEventExchange,shortLinkAddMappingBindingKey,null);
}
/**
* 新增短链api 普通队列,用于被监听
*/
@Bean
public Queue shortLinkAddLinkQueue(){
return new Queue(shortLinkAddLinkQueue,true,false,false);
}
/**
* 新增短链mapping 普通队列,用于被监听
*/
@Bean
public Queue shortLinkAddMappingQueue(){
return new Queue(shortLinkAddMappingQueue,true,false,false);
}
DBConfig
public class ShardingDBConfig {
/**
* 存储数据库位置编号
*/
private static final List<String> dbPrefixList = new ArrayList<>();
//配置启用那些库的前缀
static {
dbPrefixList.add("0");
dbPrefixList.add("1");
dbPrefixList.add("a");
}
/**
* 获取随机的前缀
*
* @return
*/
public static String getRandomDBPrefix(String code) {
int hashCode = code.hashCode();
int index = Math.abs(hashCode) % dbPrefixList.size();
return dbPrefixList.get(index);
}
}
public class ShardingTableConfig {
/**
* 存储数据表位置编号
*/
private static final List<String> tableSuffixList = new ArrayList<>();
//配置启用那些表的后缀
static {
tableSuffixList.add("0");
tableSuffixList.add("a");
}
/**
* 获取随机的后缀
*
* @return
*/
public static String getRandomTableSuffix(String code) {
int hashCode = code.hashCode();
int index = Math.abs(hashCode) % tableSuffixList.size();
return tableSuffixList.get(index);
}
}
POJO
public class EventMessage implements Serializable {
/**
* 消息队列的消息id
*/
private String messageId;
/**
* 事件类型
*/
private String eventMessageType;
/**
* 业务id
*/
private String bizId;
/**
* 账号
*/
private Long accountNo;
/**
* 消息体
*/
private String content;
/**
* 备注
*/
private String remark;
}
更新全流程
Controoler
/**
* 更新短链
* @param request
* @return
*/
@PostMapping("update")
public JsonData update(@RequestBody ShortLinkUpdateRequest request){
JsonData jsonData = shortLinkService.update(request);
return jsonData;
}
Service
Producer
@Override
public JsonData update(ShortLinkUpdateRequest request) {
Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
.content(JsonUtil.obj2Json(request))
.messageId(IDUtil.geneSnowFlakeID().toString())
.eventMessageType(EventMessageType.SHORT_LINK_UPDATE.name())
.build();
rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(),
rabbitMQConfig.getShortLinkUpdateRoutingKey(), eventMessage);
return JsonData.buildSuccess();
}
Consumer
@Override
public boolean handlerUpdateShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
ShortLinkUpdateRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkUpdateRequest.class);
//短链域名校验
DomainDO domainDO = checkDomain(request.getDomainType(), request.getDomainId(), accountNo);
if (EventMessageType.SHORT_LINK_UPDATE_LINK.name().equalsIgnoreCase(messageType)) {
ShortLinkDO shortLinkDO = ShortLinkDO.builder()
.accountNo(accountNo).code(request.getCode()).title(request.getTitle())
.domain(domainDO.getValue()).build();
int rows = shortLinkManager.update(shortLinkDO);
log.debug("更新C端短链,row:{}", rows);
return true;
} else if (EventMessageType.SHORT_LINK_UPDATE_MAPPING.name().equalsIgnoreCase(messageType)) {
GroupCodeMappingDO mappingDO = GroupCodeMappingDO.builder().accountNo(accountNo).code(request.getCode()).title(request.getTitle())
.domain(domainDO.getDomainType()).build();
int rows = groupCodeMappingManager.update(mappingDO);
log.debug("更新B端短链,row:{}", rows);
return true;
}
return false;
}
RabbitMQ
Listener
Link
@Component
@Slf4j
@RabbitListener(queuesToDeclare = {@Queue("short_link.update.link.queue")})
public class ShortLinkUpdateLinkMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) {
log.info("监听到消息");
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_LINK.name());
shortLinkService.handlerUpdateShortLink(eventMessage);
} catch (Exception e) {
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
}
}
Mapping
@Component
@Slf4j
@RabbitListener(queuesToDeclare = {@Queue("short_link.update.mapping.queue")})
public class ShortLinkUpdateMappingMQListener {
@Autowired
private ShortLinkService shortLinkService;
@RabbitHandler
public void shortLinkHandler(EventMessage eventMessage, Message message, Channel channel) {
log.info("监听消息:{}", message);
try {
eventMessage.setEventMessageType(EventMessageType.SHORT_LINK_UPDATE_MAPPING.name());
shortLinkService.handlerUpdateShortLink(eventMessage);
} catch (Exception e) {
log.info("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功:{}", eventMessage);
}
}
Config
//更新短链相关配置====================================
/**
* 更新短链 队列
*/
private String shortLinkUpdateLinkQueue="short_link.update.link.queue";
/**
* 更新短链映射 队列
*/
private String shortLinkUpdateMappingQueue="short_link.update.mapping.queue";
/**
* 更新 短链具体的routingKey,【发送消息使用】
*/
private String shortLinkUpdateRoutingKey="short_link.update.link.mapping.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 link 消费者
*/
private String shortLinkUpdateLinkBindingKey="short_link.update.link.*.routing.key";
/**
* topic类型的binding key,用于绑定队列和交换机,是用于 mapping 消费者
*/
private String shortLinkUpdateMappingBindingKey="short_link.update.*.mapping.routing.key";
/**
* 更新操作 短链api队列 和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkUpdateApiBinding(){
return new Binding(shortLinkUpdateLinkQueue,Binding.DestinationType.QUEUE,
shortLinkEventExchange,shortLinkUpdateLinkBindingKey,null);
}
/**
* 更新操作 短链mapping队列和交换机的绑定关系建立
*/
@Bean
public Binding shortLinkUpdateMappingBinding(){
return new Binding(shortLinkUpdateMappingQueue,Binding.DestinationType.QUEUE,
shortLinkEventExchange,shortLinkUpdateMappingBindingKey,null);
}
/**
* 更新操作 短链api 普通队列,用于被监听
*/
@Bean
public Queue shortLinkUpdateLinkQueue(){
return new Queue(shortLinkUpdateLinkQueue,true,false,false);
}
/**
* 更新操作 短链mapping 普通队列,用于被监听
*/
@Bean
public Queue shortLinkUpdateMappingQueue(){
return new Queue(shortLinkUpdateMappingQueue,true,false,false);
}
删除全流程
删除流程与更新类同,注意逻辑删除字段
读取全流程
controller
shortLinkCode:02BWri40
/**
* 解析 301还是302,这边是返回http code是302
* <p>
* 知识点一,为什么要用 301 跳转而不是 302 呐?
* <p>
* 301 是永久重定向,302 是临时重定向。
* <p>
* 短地址一经生成就不会变化,所以用 301 是同时对服务器压力也会有一定减少
* <p>
* 但是如果使用了 301,无法统计到短地址被点击的次数。
* <p>
* 所以选择302虽然会增加服务器压力,但是有很多数据可以获取进行分析
*
* @param linkCode
* @return
*/
@GetMapping(path = "/{shortLinkCode}")
public void dispatch(@PathVariable(name = "shortLinkCode") String shortLinkCode,
HttpServletRequest request, HttpServletResponse response) {
try {
log.info("短链码:{}", shortLinkCode);
//判断短链码是否合规
if (isLetterDigit(shortLinkCode)) {
//查找短链
ShortLinkVO shortLinkVO = shortLinkService.parseShortLinkCode(shortLinkCode);
//判断是否过期和可用
if (isVisitable(shortLinkVO)) {
String originalUrl = CommonUtil.removeUrlPrefix(shortLinkVO.getOriginalUrl());
response.setHeader("Location", originalUrl);
//302跳转
response.setStatus(HttpStatus.FOUND.value());
} else {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
}
} catch (Exception e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
Util
/**
* 移除URL前缀
* @param url
* @return
*/
public static String removeUrlPrefix(String url){
String originalUrl = url.substring(url.indexOf("&")+1);
return originalUrl;
}
/**
* 仅包括数字和字母
*
* @param str
* @return
*/
private static boolean isLetterDigit(String str) {
String regex = "^[a-z0-9A-Z]+$";
return str.matches(regex);
}
/**
* 判断短链是否可用
*
* @param shortLinkVO
* @return
*/
private static boolean isVisitable(ShortLinkVO shortLinkVO) {
if ((shortLinkVO != null && shortLinkVO.getExpired().getTime() > CommonUtil.getCurrentTimestamp())) {
if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
return true;
}
} else if ((shortLinkVO != null && shortLinkVO.getExpired().getTime() == -1)) {
if (ShortLinkStateEnum.ACTIVE.name().equalsIgnoreCase(shortLinkVO.getState())) {
return true;
}
}
return false;
}