18 changed files with 573 additions and 25 deletions
@ -0,0 +1,17 @@ |
|||||
|
package cn.nla.common.enums; |
||||
|
|
||||
|
public enum StockTaskStateEnum { |
||||
|
/** |
||||
|
* 锁定 |
||||
|
*/ |
||||
|
LOCK, |
||||
|
/** |
||||
|
* 完成 |
||||
|
*/ |
||||
|
FINISH, |
||||
|
/** |
||||
|
* 取消,释放库存 |
||||
|
*/ |
||||
|
CANCEL; |
||||
|
|
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
package cn.nla.common.model; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* 消息对象 |
||||
|
**/ |
||||
|
@Data |
||||
|
public class CouponRecordMessage { |
||||
|
/** |
||||
|
* 消息id |
||||
|
*/ |
||||
|
private String messageId; |
||||
|
/** |
||||
|
* 订单号 |
||||
|
*/ |
||||
|
private String outTradeNo; |
||||
|
/** |
||||
|
* 库存锁定任务id |
||||
|
*/ |
||||
|
private Long taskId; |
||||
|
} |
@ -0,0 +1,100 @@ |
|||||
|
package cn.nla.coupon.config; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.springframework.amqp.core.Binding; |
||||
|
import org.springframework.amqp.core.Exchange; |
||||
|
import org.springframework.amqp.core.Queue; |
||||
|
import org.springframework.amqp.core.TopicExchange; |
||||
|
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; |
||||
|
import org.springframework.amqp.support.converter.MessageConverter; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Configuration |
||||
|
@Data |
||||
|
public class RabbitMQConfig { |
||||
|
/** |
||||
|
* 交换机 |
||||
|
*/ |
||||
|
@Value("${mq.config.coupon_event_exchange}") |
||||
|
private String eventExchange; |
||||
|
/** |
||||
|
* 第一个队列 延迟队列, |
||||
|
*/ |
||||
|
@Value("${mq.config.coupon_release_delay_queue}") |
||||
|
private String couponReleaseDelayQueue; |
||||
|
/** |
||||
|
* 第一个队列的路由key |
||||
|
* 进入队列的路由key |
||||
|
*/ |
||||
|
@Value("${mq.config.coupon_release_delay_routing_key}") |
||||
|
private String couponReleaseDelayRoutingKey; |
||||
|
/** |
||||
|
* 第二个队列,被监听恢复库存的队列 |
||||
|
*/ |
||||
|
@Value("${mq.config.coupon_release_queue}") |
||||
|
private String couponReleaseQueue; |
||||
|
/** |
||||
|
* 第二个队列的路由key |
||||
|
* |
||||
|
* 即进入死信队列的路由key |
||||
|
*/ |
||||
|
@Value("${mq.config.coupon_release_routing_key}") |
||||
|
private String couponReleaseRoutingKey; |
||||
|
/** |
||||
|
* 过期时间 |
||||
|
*/ |
||||
|
@Value("${mq.config.ttl}") |
||||
|
private Integer ttl; |
||||
|
/** |
||||
|
* 消息转换器 |
||||
|
*/ |
||||
|
@Bean |
||||
|
public MessageConverter messageConverter(){ |
||||
|
return new Jackson2JsonMessageConverter(); |
||||
|
} |
||||
|
/** |
||||
|
* 创建交换机 Topic类型,也可以用dirct路由 |
||||
|
* 一般一个微服务一个交换机 |
||||
|
*/ |
||||
|
@Bean |
||||
|
public Exchange couponEventExchange(){ |
||||
|
return new TopicExchange(eventExchange,true,false); |
||||
|
} |
||||
|
/** |
||||
|
* 延迟队列 |
||||
|
*/ |
||||
|
@Bean |
||||
|
public Queue couponReleaseDelayQueue(){ |
||||
|
Map<String,Object> args = new HashMap<>(3); |
||||
|
args.put("x-message-ttl",ttl); |
||||
|
args.put("x-dead-letter-routing-key",couponReleaseRoutingKey); |
||||
|
args.put("x-dead-letter-exchange",eventExchange); |
||||
|
return new Queue(couponReleaseDelayQueue,true,false,false,args); |
||||
|
} |
||||
|
/** |
||||
|
* 死信队列,普通队列,用于被监听 |
||||
|
*/ |
||||
|
@Bean |
||||
|
public Queue couponReleaseQueue(){ |
||||
|
return new Queue(couponReleaseQueue,true,false,false); |
||||
|
} |
||||
|
/** |
||||
|
* 第一个队列,即延迟队列的绑定关系建立 |
||||
|
*/ |
||||
|
@Bean |
||||
|
public Binding couponReleaseDelayBinding(){ |
||||
|
return new Binding(couponReleaseDelayQueue,Binding.DestinationType.QUEUE,eventExchange,couponReleaseDelayRoutingKey,null); |
||||
|
} |
||||
|
/** |
||||
|
* 死信队列绑定关系建立 |
||||
|
*/ |
||||
|
@Bean |
||||
|
public Binding couponReleaseBinding(){ |
||||
|
return new Binding(couponReleaseQueue,Binding.DestinationType.QUEUE,eventExchange,couponReleaseRoutingKey,null); |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
package cn.nla.coupon.feign; |
||||
|
|
||||
|
import cn.nla.common.util.JsonData; |
||||
|
import org.springframework.cloud.openfeign.FeignClient; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* Feign调用优惠券服务接口 |
||||
|
*/ |
||||
|
@FeignClient(name = "nla-order-service") |
||||
|
public interface OrderFeignService { |
||||
|
/** |
||||
|
* 查询订单状态 |
||||
|
*/ |
||||
|
@GetMapping("/odr/product/v1/query_state") |
||||
|
JsonData queryProductOrderState(@RequestParam("out_trade_no") String outTradeNo); |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
package cn.nla.coupon.model.request; |
||||
|
|
||||
|
import io.swagger.annotations.ApiModel; |
||||
|
import io.swagger.annotations.ApiModelProperty; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@ApiModel(value = "优惠券锁定对象",description = "优惠券锁定对象") |
||||
|
@Data |
||||
|
public class LockCouponRecordRequest { |
||||
|
/** |
||||
|
* 优惠券记录id列表 |
||||
|
*/ |
||||
|
@ApiModelProperty(value = "优惠券记录id列表",example = "[1,2,3]") |
||||
|
private List<Long> lockCouponRecordIds; |
||||
|
/** |
||||
|
* 订单号 |
||||
|
*/ |
||||
|
@ApiModelProperty(value = "订单号",example = "3234fw234rfd232") |
||||
|
private String orderOutTradeNo; |
||||
|
} |
@ -0,0 +1,66 @@ |
|||||
|
package cn.nla.coupon.mq; |
||||
|
|
||||
|
import cn.nla.common.model.CouponRecordMessage; |
||||
|
import cn.nla.coupon.service.CouponRecordService; |
||||
|
import com.rabbitmq.client.Channel; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.amqp.core.Message; |
||||
|
import org.springframework.amqp.rabbit.annotation.RabbitHandler; |
||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import javax.annotation.Resource; |
||||
|
import java.io.IOException; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Component |
||||
|
@RabbitListener(queues = "${mq.config.coupon_release_queue}") |
||||
|
public class CouponMQListener { |
||||
|
|
||||
|
@Resource |
||||
|
private CouponRecordService couponRecordService; |
||||
|
|
||||
|
// @Resource
|
||||
|
// private RedissonClient redissonClient;
|
||||
|
|
||||
|
/** |
||||
|
* 重复消费-幂等性 |
||||
|
* <p> |
||||
|
* 消费失败,重新入队后最大重试次数: |
||||
|
* 如果消费失败,不重新入队,可以记录日志,然后插到数据库人工排查 |
||||
|
* <p> |
||||
|
* 消费者这块还有啥问题,大家可以先想下,然后给出解决方案 |
||||
|
*/ |
||||
|
@RabbitHandler |
||||
|
public void releaseCouponRecord(CouponRecordMessage recordMessage, Message message, Channel channel) throws IOException { |
||||
|
//防止同个解锁任务并发进入;如果是串行消费不用加锁;加锁有利也有弊,看项目业务逻辑而定
|
||||
|
//Lock lock = redissonClient.getLock("lock:coupon_record_release:"+recordMessage.getTaskId());
|
||||
|
//lock.lock();
|
||||
|
log.info("监听到消息:releaseCouponRecord消息内容:{}", recordMessage); |
||||
|
long msgTag = message.getMessageProperties().getDeliveryTag(); |
||||
|
boolean flag = couponRecordService.releaseCouponRecord(recordMessage); |
||||
|
try { |
||||
|
if (flag) { |
||||
|
//确认消息消费成功
|
||||
|
channel.basicAck(msgTag, false); |
||||
|
} else { |
||||
|
log.error("释放优惠券失败 flag=false,{}", recordMessage); |
||||
|
channel.basicReject(msgTag, true); |
||||
|
} |
||||
|
} catch (IOException e) { |
||||
|
log.error("释放优惠券记录异常:{},msg:{}", e, recordMessage); |
||||
|
channel.basicReject(msgTag, true); |
||||
|
} |
||||
|
// finally {
|
||||
|
// lock.unlock();
|
||||
|
// }
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
// @RabbitHandler
|
||||
|
// public void releaseCouponRecord2(String msg,Message message, Channel channel) throws IOException {
|
||||
|
// log.info(msg);
|
||||
|
// channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
|
||||
|
// }
|
||||
|
|
||||
|
} |
@ -0,0 +1,74 @@ |
|||||
|
server: |
||||
|
port: 9002 |
||||
|
spring: |
||||
|
application: |
||||
|
name: nla-coupon-service |
||||
|
# 缓存 |
||||
|
redis: |
||||
|
host: 127.0.0.1 |
||||
|
port: 6379 |
||||
|
database: 0 |
||||
|
password: yuan123456 |
||||
|
#数据库配置 |
||||
|
datasource: |
||||
|
driver-class-name: com.mysql.cj.jdbc.Driver |
||||
|
url: jdbc:mysql://117.72.43.105:3306/p_nla_coupon?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai |
||||
|
username: root |
||||
|
password: Yuan625621105. |
||||
|
#注册中心地址 |
||||
|
cloud: |
||||
|
nacos: |
||||
|
discovery: |
||||
|
server-addr: 117.72.43.105:8848 |
||||
|
# username: nacos |
||||
|
# password: sW5U%pxecL#p |
||||
|
# namespace: yjs |
||||
|
|
||||
|
#消息队列 |
||||
|
rabbitmq: |
||||
|
host: 192.168.30.130 |
||||
|
port: 5672 |
||||
|
virtual-host: / |
||||
|
password: admin |
||||
|
username: admin |
||||
|
#开启手动确认消息 |
||||
|
listener: |
||||
|
simple: |
||||
|
acknowledge-mode: manual |
||||
|
|
||||
|
#配置plus打印sql⽇志 |
||||
|
mybatis-plus: |
||||
|
configuration: |
||||
|
log-impl: |
||||
|
org.apache.ibatis.logging.stdout.StdOutImpl |
||||
|
#设置⽇志级别,ERROR/WARN/INFO/DEBUG,默认是INFO以上才显示 |
||||
|
#logging: |
||||
|
# level: |
||||
|
# root: INFO |
||||
|
|
||||
|
|
||||
|
#seata配置 |
||||
|
#seata: |
||||
|
# tx-service-group: ${spring.application.name}-group |
||||
|
# service: |
||||
|
# grouplist: |
||||
|
# nla: 127.0.0.1:8091 |
||||
|
# vgroup-mapping: |
||||
|
# nla-coupon-service-group: nla |
||||
|
|
||||
|
|
||||
|
##自定义消息队列配置,发送锁定库存消息-》延迟exchange-》lock.queue-》死信exchange-》release.queue |
||||
|
mq: |
||||
|
config: |
||||
|
#延迟队列,不能被监听消费 |
||||
|
coupon_release_delay_queue: coupon.release.delay.queue |
||||
|
#延迟队列的消息过期后转发的队列 |
||||
|
coupon_release_queue: coupon.release.queue |
||||
|
#交换机 |
||||
|
coupon_event_exchange: coupon.event.exchange |
||||
|
#进入延迟队列的路由key |
||||
|
coupon_release_delay_routing_key: coupon.release.delay.routing.key |
||||
|
#消息过期,进入释放死信队列的key |
||||
|
coupon_release_routing_key: coupon.release.routing.key |
||||
|
#消息过期时间,毫秒,临时改为6分钟 |
||||
|
ttl: 360000 |
@ -0,0 +1,31 @@ |
|||||
|
package cn.nla.coupon; |
||||
|
|
||||
|
import cn.nla.common.model.CouponRecordMessage; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
import org.springframework.test.context.junit4.SpringRunner; |
||||
|
|
||||
|
import javax.annotation.Resource; |
||||
|
|
||||
|
@RunWith(SpringRunner.class) |
||||
|
@SpringBootTest(classes = CouponApplication.class) |
||||
|
@Slf4j |
||||
|
public class CouponApplicationTests { |
||||
|
|
||||
|
@Resource |
||||
|
private RabbitTemplate rabbitTemplate; |
||||
|
|
||||
|
@Test |
||||
|
public void send() { |
||||
|
// rabbitTemplate.convertAndSend("coupon.event.exchange", "coupon.release.delay.routing.key", "测试数据");
|
||||
|
CouponRecordMessage message = new CouponRecordMessage(); |
||||
|
message.setOutTradeNo("123456abc"); |
||||
|
message.setTaskId(1L); |
||||
|
rabbitTemplate.convertAndSend("coupon.event.exchange", "coupon.release.delay.routing.key", message); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
Loading…
Reference in new issue