网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
*/
@Bean
public Exchange orderEventExchange(){
return new TopicExchange(orderEventExchange,true,false);
}
/**
* 延迟队列
*/
@Bean
public Queue orderCloseDelayQueue(){
Map<String,Object> args = new HashMap<>(3);
args.put("x-dead-letter-exchange",orderEventExchange);
args.put("x-dead-letter-routing-key",orderCloseRoutingKey);
args.put("x-message-ttl",ttl);
return new Queue(orderCloseDelayQueue,true,false,false,args);
}
/**
* 死信队列,普通队列,用于被监听
*/
@Bean
public Queue orderCloseQueue(){
return new Queue(orderCloseQueue,true,false,false);
}
/**
* 第一个队列,即延迟队列的绑定关系建立
* @return
*/
@Bean
public Binding orderCloseDelayBinding(){
return new Binding(orderCloseDelayQueue,Binding.DestinationType.QUEUE,orderEventExchange,orderCloseDelayRoutingKey,null);
}
/**
* 死信队列绑定关系建立
* @return
*/
@Bean
public Binding orderCloseBinding(){
return new Binding(orderCloseQueue,Binding.DestinationType.QUEUE,orderEventExchange,orderCloseRoutingKey,null);
}
}
@Component
@Slf4j
@RabbitListener(queuesToDeclare = {@Queue(“order.close.queue”),@Queue(“order.update.queue”)})
public class ProductOrderMQListener {
@Autowired
private ProductOrderService productOrderService;
@RabbitHandler
public void productOrderHandler(EventMessage eventMessage, Message message, Channel channel){
log.info("监听到消息ProductrOrderMQListener message:{}", eventMessage);
try {
productOrderService.handleProductOrderMessage(eventMessage);
} catch (Exception e) {
log.error("消费失败:{}", eventMessage);
throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);
}
log.info("消费成功");
}
}
3、问题:海量数据场景下冗余双写唯一码生成方式探讨,以短链码(订单号)讲解。
方案1:
**生产者端生成短链码**。先在数据库查询短链码是否存在,不存在的话通过redis设计一个分布式锁key=code并配置过期时间(加锁失败则重新生成),存在则重新生成,重复以上操作。随后再C端消费者和B端消费者均写入成功后再进行解锁。
![](https://img-blog.csdnimg.cn/direct/fc6d217617ef41e9aaaa2c587a73c129.png)
缺点:性能比较低,用户需要等待所生成的短链上锁,最终入库解锁才能返回给用户,等待时间较长。
方案2:
**消费者(C端/B端)生成短链码**。用户请求后立即返回消息给客户,随后消费者(C端/B端)各自进行加锁写入数据库,确保两者冲突时能遵照统一ver版本自增机制,重新生成短链码。
![](https://img-blog.csdnimg.cn/direct/295dc288e08b4b86ae628bcaf73ae79c.png)
/**
* 如果短链码重复,则调用这个方法
* url前缀的编号递增1
* 如果还是用雪花算法,则容易C端和B端不一致,所以才用编号递增+1的方式
* 123456789&http://baidu.com/download.html
*
* @param url
* @return
*/
public static String addUrlPrefixVersion(String url) {
String urlPrefix = url.substring(0, url.indexOf(“&”));
String originalUrl = url.substring(url.indexOf(“&”) + 1);
Long newUrlPrefix = Long.parseLong(urlPrefix) + 1;
String newUrl = newUrlPrefix + “&” + originalUrl;
return newUrl;
}
/**
* 判断短链域名是否合法
* 判断组名是否合法
* 生成长链摘要
* 生成短链码
* 加锁
* 查询短链码是否存在
* 构建短链对象
* 保存数据库
*
* @param eventMessage
* @return
*/
@Override
public boolean handlerAddShortLink(EventMessage eventMessage) {
Long accountNo = eventMessage.getAccountNo();
String messageType = eventMessage.getEventMessageType();
String content = eventMessage.getContent();
ShortLinkAddRequest addRequest = JsonUtil.json2Obj(content, 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());
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 (EventMessageTypeEnum.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {
//先判断短链码是否被占用
ShortLinkDO shortLinkDOInDB = shortLinkManager.findByShortLinkCode(shortLinkCode);
if (shortLinkDOInDB == null) {
//扣减流量包
boolean reduceFlag = reduceTraffic(eventMessage,shortLinkCode);
//扣减成功才创建流量包
if(reduceFlag){
//链式调用
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 (EventMessageTypeEnum.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {
//先判断短链码是否被占用
GroupCodeMappingDO groupCodeMappingDOInDB = groupCodeMappingManager.findByCodeAndGroupId(shortLinkCode, linkGroupDO.getId(), accountNo);
if (groupCodeMappingDOInDB == null) {
//B端处理
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;
}
4、问题:海量数据高并发场景下冗余双写消费端生成唯一码错乱问题处理,以短链码(订单号)讲解。冲突详细如下:
1)用户A生成短链码AABBCC ,C端先插入,B端还没插入
2)用户B也生成短链码AABBCC ,B端先插入,C端还没插入
3)用户A生成短链码AABBCC ,B端插入 (死锁,相互等待)
4)用户B生成短链码AABBCC ,C端插入(死锁,相互等待)
那么如何让1、3可以成功, 2、4可以成功呢?
方案1:
**添加本地锁**,synchronize、lock等,再锁内处理事务。JDK指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的,synchronized 和 ReentrantLock 都是可重入锁
缺点:锁在当前进程内,集群部署下依旧存在问题。
方案2:
**添加分布式锁**,redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql。当一个线程获取对象锁之后,其他节点的同个业务线程可以再次获取本对象上的锁。
![](https://img-blog.csdnimg.cn/direct/66eaa172586c4c6f822ecfb2ac7fc1c2.png)
设计分布式锁应该考虑:
1)排他性。在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行;
2)容错性。分布式锁一定能得到释放,比如客户端奔溃或者网络中断;
3)满足可重入、高性能、高可用;
4)注意分布式锁的开销、锁粒度;
分布式锁设计(redis):
key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill\_商品ID” 。value就可以使用固定值,比如设置成1。短链码可以:short\_link:code:xxxx,基于redis实现分布式锁,文档[http://www.redis.cn/commands.html#string]( )
methodA(){
String key = “short_link:code:abcdef”
if(setnx(key,1) == 1){
expire(key,30,TimeUnit.MILLISECONDS)
try {
//做对应的业务逻辑
} finally {
del(key)
}
}else{
//睡眠100毫秒,然后自旋调用本方法
methodA()
}
}
问题:多个命令之间不是原子性操作,如`setnx`和`expire`之间,如果`setnx`成功,但是`expire`失败,且宕机了,则这个资源就是死锁
核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用判断和设置等怎么保证原子性。
多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
使用原子命令:设置和配置过期时间 setnx / setex
如: set key 1 ex 30 nx
java代码里面
String key = “short_link:code:abcdef”
redisTemplate.opsForValue().setIfAbsent(key,1,30,TimeUnit.MILLISECONDS)
//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;";
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
链图片转存中…(img-mAlKlyR2-1715266836926)]
[外链图片转存中…(img-qS8YU8ov-1715266836926)]
[外链图片转存中…(img-QN3fOM2X-1715266836927)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新