基于redis幂等机制,保证优惠券不会重复发放-13

说明:儒猿技术窝基于RocketMQ的互联网酒店预订系统项目实战学习笔记

1、通过Redis 解决幂等性问题的思路

1.解决优惠券发放幂等性的问题需要回到消费者处理消息本身
-因为网络抖动生产者(LoginProducer)产生了重复的消息,CouponConsumer会重复消费两条消息

-消费者(CouponConsumer本身)由于下线没有通知broker CONSUMER_SUCCESS,导致重启后重复消费

2.如果在消费者(CouponConsumer)每次处理消息的时候都将这个消息存放到Redis上进行缓存,在每次发放优惠券之前都会询问“是否存在相
同消息?”,只有在“否”的情况下才执行“发放优惠券”。这里的“否”也就是不存在相同优惠券的意思,于是就避免了优惠券的重复发放

2、代码中如何使用Redis解决问题

1.通过redisApi.setnx方法(已经由儒猿团队进行封装)将消息信息保存到Redis中。设置了“第一次重复登录”的前缀+用户Id(userId)作
为Key,后面的用户名(userId)、电话号码(phoneNumber)作为Value

2.利用setnx的函数特性,当reponse返回成功“SUCCESS”并且data为”FALSE“说明Redis中对应的Key已经有Value 了,也就说明Redis中
已经存在这条消息了,消费者已经处理过这条“第一次登录”的记录了。这种情况下,只记录一个日志,而不进行后续的操作

2.如果返回的是“SUCCESS”并且data为“true”,说明Redis没有记录这条消息,也就是消费者第一次处理这个消息,那么就需要更新数据库中
用户和优惠券的关系,从而完成给用户发放优惠券的操作

@Component
public class FirstLoginMessageListener implements MessageListenerConcurrently {

    private static final Logger LOGGER = LoggerFactory.getLogger(FirstLoginMessageListener.class);

    /**
     * redis dubbo服务
     */
    @Reference(version = "1.0.0",
            interfaceClass = RedisApi.class,
            cluster = "failfast")
    private RedisApi redisApi;

    /**
     * 优惠券服务service组件
     */
    @Autowired
    private CouponService couponService;

    /**
     * 第一次登陆下发的优惠券id
     */
    @Value("${first.login.couponId}")
    private Integer firstLoginCouponId;

    /**
     * 第一次登陆优惠券有效天数
     */
    @Value("${first.login.coupon.day}")
    private Integer firstLoginCouponDay;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        Integer userId = null;
        String phoneNumber = null;
        for (MessageExt msg : msgs) {
            String body = new String(msg.getBody(), StandardCharsets.UTF_8);
            try {
                LOGGER.info("received login success message:{}", body);
                // 第一次登陆消息内容
                FirstLoginMessageDTO firstLoginMessageDTO = JSON.parseObject(body, FirstLoginMessageDTO.class);
                // 用户id
                userId = firstLoginMessageDTO.getUserId();
                // 手机号
                phoneNumber = firstLoginMessageDTO.getPhoneNumber();

                // 通过redis保证幂等
                CommonResponse<Boolean> response = redisApi.setnx(RedisKeyConstant.FIRST_LOGIN_DUPLICATION_KEY_PREFIX + userId,
                                                                  String.valueOf(userId),
                                                                  phoneNumber,
                                                                  LittleProjectTypeEnum.ROCKETMQ);
                if (Objects.equals(response.getCode(), ErrorCodeEnum.FAIL.getCode())) {
                    // 请求redis dubbo接口失败
                    LOGGER.info("consumer first login message redis dubbo interface fail userId:{}", userId);
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }

                // redis操作成功
                if (Objects.equals(response.getCode(), ErrorCodeEnum.SUCCESS.getCode())
                        && Objects.equals(response.getData(), Boolean.FALSE)) {
                    // 重复消费登录消息 返回
                    LOGGER.info("duplicate consumer first login message userId:{}", userId);
                } else {
                    // 未重复消费 分发权益
                    couponService.distributeCoupon(firstLoginMessageDTO.getBeid(),
                                                   firstLoginMessageDTO.getUserId(),
                                                   firstLoginCouponId,
                                                   firstLoginCouponDay,
                                                   0,
                                                   phoneNumber);
                    LOGGER.info("distribute userId:{} first login coupon end", userId);
                }
            } catch (Exception e) {
                // 消费失败,删除redis中幂等key
                if (userId != null) {
                    redisApi.del(RedisKeyConstant.FIRST_LOGIN_DUPLICATION_KEY_PREFIX + userId,
                                 phoneNumber,
                                 LittleProjectTypeEnum.ROCKETMQ);
                }
                // 消费失败
                LOGGER.info("received login success message:{}, consumer fail", body);
                // Failure consumption,later try to consume 消费失败,以后尝试消费
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }
        }

        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

3、兜底方案

在公司中实战时需要考虑兜底方案
当redis宕机后如何去保证幂等?常用的兜底方案数据库唯一键,本案例中下发优惠券则需要用户id+优惠券id组成唯一键

版权声明:本文为zhangruibo_code原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。