冗余双写方案下数据一致性问题解决及延申问题处理方案

注意:消息消费确认使用自动确认方式(acknowledge-mode=auto)

2)Rabbitmq的延迟队列采用死信队列方式解决,即被投递的队列无消费者订阅,所进入该队列的消息超时未消费时,会重新投递到另外的队列,超时时间则就是延迟时间。

@Configuration
@Data
public class RabbitMQConfig {


    /**
     * 交换机
     */
    private String orderEventExchange="order.event.exchange";


    /**
     * 延迟队列, 不能被监听消费
     */
    private String orderCloseDelayQueue="order.close.delay.queue";

    /**
     * 关单队列, 延迟队列的消息过期后转发的队列,被消费者监听
     */
    private String orderCloseQueue="order.close.queue";


    /**
     * 进入延迟队列的路由key
     */
    private String orderCloseDelayRoutingKey="order.close.delay.routing.key";


    /**
     * 进入死信队列的路由key,消息过期进入死信队列的key
     */
    private String orderCloseRoutingKey="order.close.routing.key";

    /**
     * 过期时间 毫秒,临时改为1分钟定时关单
     */
    private Integer ttl=1000*60;

    /**
     * 消息转换器
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }


    /**
     * 创建交换机 Topic类型,也可以用dirct路由
     * 一般一个微服务一个交换机
     * @return
     */
    @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端消费者均写入成功后再进行解锁。

缺点:性能比较低,用户需要等待所生成的短链上锁,最终入库解锁才能返回给用户,等待时间较长。

方案2:

消费者(C端/B端)生成短链码。用户请求后立即返回消息给客户,随后消费者(C端/B端)各自进行加锁写入数据库,确保两者冲突时能遵照统一ver版本自增机制,重新生成短链码。

 /**
     * 如果短链码重复,则调用这个方法
     * 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。当一个线程获取对象锁之后,其他节点的同个业务线程可以再次获取本对象上的锁。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

b2ac7fc1c2.png)

[外链图片转存中…(img-z6blCJGv-1714494697440)]
[外链图片转存中…(img-K9gO2Ije-1714494697440)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值