基于redis幂等机制,保证优惠券不会重复发放
说明:儒猿技术窝基于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版权协议,转载请附上原文出处链接和本声明。