1.RocketMQ简介
RocketMQ是阿里巴巴2016年MQ中间件,使用Java语言开发,RocketMQ 是一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。同时,广泛应用于多个领域,包括异步通信解耦、企业解决方案、金融支付、电信、电子商务、快递物流、广告营销、社交、即时通信、移动应用、手游、视频、物联网、车联网等。
2.为什么要使用MQ
①.要做到系统解耦,当新的模块进来时,可以做到代码改动最小; 能够解耦
②.设置流程缓冲池,可以让后端系统按自身吞吐能力进行消费,不被冲垮; 能够削峰,限流
③.强弱依赖梳理能把非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步
3.各个MQ产品的比较
4.RocketMQ重要概念
Producer:消息的发送者,生产者;举例:发件人
Consumer:消息接收者,消费者;举例:收件人
Broker:暂存和传输消息的通道;举例:快递
NameServer:管理Broker;举例:各个快递公司的管理机构相当于broker的注册中心,保留了broker的信息
Queue:队列,消息存放的位置,一个Broker中可以有多个队列
Topic:主题,消息的分类
5.生产和消费理解【重点】
6.RocketMQ安装
6.1下载RocketMQ
下载地址:rocketmq.apache.org/dowloading/…
6.2上传服务器
mkdir rocketmq
6.3解压
yum install unzip
unzip rocketmq-all-4.9.2-bin-release.zip
6.4配置环境变量
vim /etc/profile
export NAMESRV_ADDR=阿里云公网IP:9876
6.5修改nameServer的运行脚本
进入bin目录下,修改runserver.sh文件,将71行和76行的Xms和Xmx等改小一点
vim runserver.sh
6.6修改broker的运行脚本
进入bin目录下,修改runbroker.sh文件,修改67行
6.7修改broker的配置文件
进入conf目录下,修改broker.conf文件
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
namesrvAddr=localhost:9876
autoCreateTopicEnable=true
brokerIP1=阿里云公网IP
添加参数解释
namesrvAddr:nameSrv地址可以写localhost因为nameSrv和broker在一个服务器
autoCreateTopicEnable:自动创建主题,不然需要手动创建出来
brokerIP1:broker也需要一个公网ip,如果不指定,那么是阿里云的内网地址,我们再本地无法连接使用
6.8启动
一次运行两条命令
启动nameSrv
nohup sh bin/mqnamesrv > ./logs/namesrv.log &
启动broker 这里的-c是指定使用的配置文件
nohup sh bin/mqbroker -c conf/broker.conf > ./logs/broker.log &
查看启动结果
6.9RocketMQ控制台的安装RocketMQ-Console
然后运行
nohup java -jar ./rocketmq-dashboard-1.0.0.jar rocketmq.config.namesrvAddr=127.0.0.1:9876 > ./rocketmq-4.9.3/logs/dashboard.log &
命令拓展:--server.port指定运行的端口
--rocketmq.config.namesrvAddr=127.0.0.1:9876 指定namesrv地址
7.RocketMQ快速入门
7.1加入依赖
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.2</version>
<!--docker的用下面这个版本-->
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>
7.2编写生产者
/**
* 测试生产者
*
* @throws Exception
*/
@Test
public void testProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
for (int i = 0; i < 10; i++) {
// 创建消息
// 第一个参数:主题的名字
// 第二个参数:消息内容
Message msg = new Message("TopicTest", ("Hello RocketMQ " + i).getBytes());
SendResult send = producer.send(msg);
System.out.println(send);
}
// 关闭实例
producer.shutdown();
}
7.3编写消费者
/**
* 测试消费者
*
* @throws Exception
*/
@Test
public void testConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 *表示没有过滤参数 表示这个主题的任何消息
consumer.subscribe("TopicTest", "*");
// 注册一个消费监听 MessageListenerConcurrently 是多线程消费,默认20个线程,可以参看consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.println(Thread.currentThread().getName() + "----" + msgs);
// 返回消费的状态 如果是CONSUME_SUCCESS 则成功,若为RECONSUME_LATER则该条消息会被重回队列,重新被投递
// 重试的时间为messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
// 也就是第一次1s 第二次5s 第三次10s .... 如果重试了18次 那么这个消息就会被终止发送给消费者
// return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
// 这个start一定要写在registerMessageListener下面
consumer.start();
System.in.read();
}
8.消费模式
MQ的消费模式可以大致分为两种,一种是推Push,一种是拉Pull。
Push是服务端【MQ】主动推送消息给客户端,优点是及时性较好,但如果客户端没有做好流控,一旦服务端推送大量消息到客户端时,就会导致客户端消息堆积甚至崩溃。
Pull是客户端需要主动到服务端取数据,优点是客户端可以依据自己的消费能力进行消费,但拉取的频率也需要用户自己控制,拉取频繁容易造成服务端和客户端的压力,拉取间隔长又容易造成消费不及时。
Push模式也是基于pull模式的,只能客户端内部封装了api,一般场景下,上游消息生产量小或者均速的时候,选择push模式。在特殊场景下,例如电商大促,抢优惠券等场景可以选择pull模式
9.RocketMQ发送同步消息
上面的快速入门就是发送同步消息,发送过后会有一个返回值,也就是mq服务器接收到消息后返回的一个确认,这种方式非常安全,但是性能上并没有这么高,而且在mq集群中,也是要等到所有的从机都复制了消息以后才会返回,所以针对重要的消息可以选择这种方式
10.RocketMQ发送异步消息
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。发送完以后会有一个异步消息通知
10.1异步消息生产者
@Test
public void testAsyncProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
Message msg = new Message("TopicTest", ("异步消息").getBytes());
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
@Override
public void onException(Throwable e) {
System.out.println("发送失败");
}
});
System.out.println("看看谁先执行");
// 挂起jvm 因为回调是异步的不然测试不出来
System.in.read();
// 关闭实例
producer.shutdown();
}
10.2异步消息消费者
@Test
public void testAsyncConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 *表示没有过滤参数 表示这个主题的任何消息
consumer.subscribe("TopicTest", "*");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
// 这里执行消费的代码 默认是多线程消费
System.out.println(Thread.currentThread().getName() + "----" + msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
11.RocketMQ发送单向消息
这种方式主要用在不关心发送结果的场景,这种方式吞吐量很大,但是存在消息丢失的风险,例如日志信息的发送
11.1单向消息生产者
@Test
public void testOnewayProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
Message msg = new Message("TopicTest", ("单向消息").getBytes());
// 发送单向消息
producer.sendOneway(msg);
// 关闭实例
producer.shutdown();
}
11.2单向消息消费者
@Test
public void testAsyncConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 *表示没有过滤参数 表示这个主题的任何消息
consumer.subscribe("TopicTest", "*");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
// 这里执行消费的代码 默认是多线程消费
System.out.println(Thread.currentThread().getName() + "----" + msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
12.RocketMQ发送延迟消息
消息放入mq后,过一段时间,才会被监听到,然后消费比如下订单业务,提交了一个订单就可以发送一个延时消息,30min后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
12.1延迟消息生产者
@Test
public void testDelayProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("http://120.79.72.142:9876");
// 启动实例
producer.start();
Message msg = new Message("TopicTest", ("延迟消息").getBytes());
// 给这个消息设定一个延迟等级
// messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
msg.setDelayTimeLevel(3);
// 发送单向消息
producer.send(msg);
// 打印时间
System.out.println(new Date());
// 关闭实例
producer.shutdown();
}
12.2延迟消息消费者
@Test
public void testAsyncConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("http://120.79.72.142:9876");
// 订阅一个主题来消费 *表示没有过滤参数 表示这个主题的任何消息
consumer.subscribe("TopicTest", "*");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
// 这里执行消费的代码 默认是多线程消费
System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
13.RocketMQ发送顺序消息
13.1创建一个订单对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
/**
* 订单id
*/
private Integer orderId;
/**
* 订单编号
*/
private Integer orderNumber;
/**
* 订单价格
*/
private Double price;
/**
* 订单号创建时间
*/
private Date createTime;
/**
* 订单描述
*/
private String desc;
}
13.2顺序消息生产者
@Test
public void testOrderlyProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
List<Order> orderList = Arrays.asList(
new Order(1, 111, 59D, new Date(), "下订单"),
new Order(2, 111, 59D, new Date(), "物流"),
new Order(3, 111, 59D, new Date(), "签收"),
new Order(4, 112, 89D, new Date(), "下订单"),
new Order(5, 112, 89D, new Date(), "物流"),
new Order(6, 112, 89D, new Date(), "拒收")
);
// 循环集合开始发送
orderList.forEach(order -> {
Message message = new Message("TopicTest", order.toString().getBytes());
try {
// 发送的时候 相同的订单号选择同一个队列
producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
// 当前主题有多少个队列
int queueNumber = mqs.size();
// 这个arg就是后面传入的 order.getOrderNumber()
Integer i = (Integer) arg;
// 用这个值去%队列的个数得到一个队列
int index = i % queueNumber;
// 返回选择的这个队列即可 ,那么相同的订单号 就会被放在相同的队列里 实现FIFO了
return mqs.get(index);
}
}, order.getOrderNumber());
} catch (Exception e) {
System.out.println("发送异常");
}
});
// 关闭实例
producer.shutdown();
}
13.3顺序消息消费者,测试时等一会即可有延迟
@Test
public void testOrderlyConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 *表示没有过滤参数 表示这个主题的任何消息
consumer.subscribe("TopicTest", "*");
// 注册一个消费监听 MessageListenerOrderly 是顺序消费 单线程消费
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
MessageExt messageExt = msgs.get(0);
System.out.println(new String(messageExt.getBody()));
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.in.read();
}
14.RocketMQ发送批量消息
Rocketmq可以一次性发送一组消息,那么这一组消息会被当做一个消息消费
14.1批量消息生产者
@Test
public void testBatchProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
List<Message> msgs = Arrays.asList(
new Message("TopicTest", "我是一组消息的A消息".getBytes()),
new Message("TopicTest", "我是一组消息的B消息".getBytes()),
new Message("TopicTest", "我是一组消息的C消息".getBytes())
);
SendResult send = producer.send(msgs);
System.out.println(send);
// 关闭实例
producer.shutdown();
}
14.2批量消息消费者
@Test
public void testBatchConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 表达式,默认是*
consumer.subscribe("TopicTest", "*");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
// 这里执行消费的代码 默认是多线程消费
System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
15.RocketMQ发送带标签的消息,消息过滤
Rocketmq提供消息过滤功能,通过tag或者key进行区分我们往一个主题里面发送消息的时候,根据业务逻辑,可能需要区分,比如带有tagA标签的被A消费,带有tagB标签的被B消费
15.1标签消息生产者
@Test
public void testTagProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
Message msg = new Message("TopicTest","tagA", "我是一个带标记的消息".getBytes());
SendResult send = producer.send(msg);
System.out.println(send);
// 关闭实例
producer.shutdown();
}
15.2标签消息消费者
@Test
public void testTagConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 表达式,默认是*,支持"tagA || tagB || tagC" 这样或者的写法 只要是符合任何一个标签都可以消费
consumer.subscribe("TopicTest", "tagA || tagB || tagC");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
// 这里执行消费的代码 默认是多线程消费
System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
System.out.println(msgs.get(0).getTags());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
15.3什么时候该用 Topic,什么时候该用 Tag?
总结:不同的业务应该使用不同的Topic如果是相同的业务里面有不同表的表现形式,那么我们要使用tag进行区分
可以从以下几个方面进行判断:
1.消息类型是否一致:如普通消息、事务消息、定时(延时)消息、顺序消息,不同的消息类型使用不同的 Topic,无法通过 Tag 进行区分。
2.业务是否相关联:没有直接关联的消息,如淘宝交易消息,京东物流消息使用不同的 Topic 进行区分;而同样是天猫交易消息,电器类订单、女装类订单、化妆品类订单的消息可以用 Tag 进行区分。
3.消息优先级是否一致:如同样是物流消息,盒马必须小时内送达,天猫超市 24 小时内送达,淘宝物流则相对会慢一些,不同优先级的消息用不同的 Topic 进行区分。
16.RocketMQ中消息的Key
在rocketmq中的消息,默认会有一个messageId当做消息的唯一标识,我们也可以给消息携带一个key,用作唯一标识或者业务标识,包括在控制面板查询的时候也可以使用messageId或者key来进行查询
16.1带key消息生产者
@Test
public void testKeyProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-group");
// 设置nameServer地址
producer.setNamesrvAddr("localhost:9876");
// 启动实例
producer.start();
Message msg = new Message("TopicTest","tagA","key", "我是一个带标记和key的消息".getBytes());
SendResult send = producer.send(msg);
System.out.println(send);
// 关闭实例
producer.shutdown();
}
16.2带key消息消费者
@Test
public void testKeyConsumer() throws Exception {
// 创建默认消费者组
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer-group");
// 设置nameServer地址
consumer.setNamesrvAddr("localhost:9876");
// 订阅一个主题来消费 表达式,默认是*,支持"tagA || tagB || tagC" 这样或者的写法 只要是符合任何一个标签都可以消费
consumer.subscribe("TopicTest", "tagA || tagB || tagC");
// 注册一个消费监听 MessageListenerConcurrently是并发消费
// 默认是20个线程一起消费,可以参看 consumer.setConsumeThreadMax()
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
// 这里执行消费的代码 默认是多线程消费
System.out.println(Thread.currentThread().getName() + "----" + new String(msgs.get(0).getBody()));
System.out.println(msgs.get(0).getTags());
System.out.println(msgs.get(0).getKeys());
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
17.RocketMQ消息重复消费问题
17.1为什么会出现重复消费问题呢?
17.2解决方案
17.3添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- 原生的api -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>3.0.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
17.4数据库配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useSSL=false
17.5测试生产者
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void repeatProducer() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("repeat-producer-group");
producer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
producer.start();
String key = UUID.randomUUID().toString();
System.out.println(key);
// 测试 发两个key一样的消息
Message m1 = new Message("repeatTopic", null, key, "扣减库存-1".getBytes());
Message m1Repeat = new Message("repeatTopic", null, key, "扣减库存-1".getBytes());
producer.send(m1);
producer.send(m1Repeat);
System.out.println("发送成功");
producer.shutdown();
}
17.6测试消费者
/**
* 幂等性(mysql的唯一索引, redis(setnx) )
* 多次操作产生的影响均和第一次操作产生的影响相同
* 新增:普通的新增操作 是非幂等的,唯一索引的新增,是幂等的
* 修改:看情况
* 查询: 是幂等操作
* 删除:是幂等操作
* ---------------------
* 我们设计一个去重表 对消息的唯一key添加唯一索引
* 每次消费消息的时候 先插入数据库 如果成功则执行业务逻辑 [如果业务逻辑执行报错 则删除这个去重表记录]
* 如果插入失败 则说明消息来过了,直接签收了
*
* @throws Exception
*/
@Test
void repeatConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("repeat-consumer-group");
consumer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
consumer.subscribe("repeatTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
// 先拿key
MessageExt messageExt = msgs.get(0);
String keys = messageExt.getKeys();
// 原生方式操作
Connection connection = null;
try {
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useSSL=false", "root", "123456");
} catch (SQLException e) {
e.printStackTrace();
}
PreparedStatement statement = null;
try {
// 插入数据库 因为我们 key做了唯一索引
statement = connection.prepareStatement("insert into order_oper_log(`type`, `order_sn`, `user`) values (1,'" + keys + "','123')");
} catch (SQLException e) {
e.printStackTrace();
}
try {
// 新增 要么成功 要么报错 修改 要么成功,要么返回0 要么报错
statement.executeUpdate();
} catch (SQLException e) {
System.out.println("executeUpdate");
if (e instanceof SQLIntegrityConstraintViolationException) {
// 唯一索引冲突异常
// 说明消息来过了
System.out.println("该消息来过了");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
e.printStackTrace();
}
// 处理业务逻辑
// 如果业务报错 则删除掉这个去重表记录 delete order_oper_log where order_sn = keys;
System.out.println(new String(messageExt.getBody()));
System.out.println(keys);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
18.RocketMQ重试机制
18.1生产者重试
@Test
public void retryProducer() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("retry-producer-group");
producer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
producer.start();
// 生产者发送消息 重试次数
producer.setRetryTimesWhenSendFailed(2);
producer.setRetryTimesWhenSendAsyncFailed(2);
String key = UUID.randomUUID().toString();
System.out.println(key);
Message message = new Message("retryTopic", "vip1", key, "我是vip666的文章".getBytes());
producer.send(message);
System.out.println("发送成功");
producer.shutdown();
}
18.2消费者重试
在消费者放return ConsumeConcurrentlyStatus.RECONSUME_LATER;后就会执行重试
上图代码中说明了,我们再实际生产过程中,一般重试3-5次,如果还没有消费成功,则可以把消息签收了,通知人工等处理
/**
* 重试的时间间隔
* 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
* 默认重试16次
* 1.能否自定义重试次数
* 2.如果重试了16次(并发模式) 顺序模式下(int最大值次)都是失败的? 是一个死信消息 则会放在一个死信主题中去 主题的名称:%DLQ%retry-consumer-group
* 3.当消息处理失败的时候 该如何正确的处理?
* --------------
* 重试的次数一般 5次
* @throws Exception
*/
@Test
public void retryConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
consumer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
consumer.subscribe("retryTopic", "*");
// 设定重试次数
consumer.setMaxReconsumeTimes(2);
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
MessageExt messageExt = msgs.get(0);
System.out.println(new Date());
System.out.println(messageExt.getReconsumeTimes());
System.out.println(new String(messageExt.getBody()));
// 业务报错了 返回null 返回 RECONSUME_LATER 都会重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
System.in.read();
}
18.3直接监听死信主题的消息
/////////// 直接监听死信主题的消息,记录下拉 通知人工接入处理
@Test
public void retryDeadConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-dead-consumer-group");
consumer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
consumer.subscribe("%DLQ%retry-consumer-group", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
MessageExt messageExt = msgs.get(0);
System.out.println(new Date());
System.out.println(new String(messageExt.getBody()));
System.out.println("记录到特别的位置 文件 mysql 通知人工处理");
// 业务报错了 返回null 返回 RECONSUME_LATER 都会重试
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
18.4第二种方案
//////////////////////// 第二种方案 用法比较多
@Test
public void retryConsumer2() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
consumer.setNamesrvAddr(MqConstant.NAME_SRV_ADDR);
consumer.subscribe("retryTopic", "*");
// 设定重试次数
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
MessageExt messageExt = msgs.get(0);
System.out.println(new Date());
// 业务处理
try {
handleDb();
} catch (Exception e) {
// 重试
int reconsumeTimes = messageExt.getReconsumeTimes();
if (reconsumeTimes >= 3) {
// 不要重试了
System.out.println("记录到特别的位置 文件 mysql 通知人工处理");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
// 业务报错了 返回null 返回 RECONSUME_LATER 都会重试
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.in.read();
}
private void handleDb() {
int i = 10 / 0;
}
19.Rocketmq集成SpringBoot
19.1 b-rocketmq-boot-p创建项目,完整的pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
19.2消息发送
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Test
void contextLoads() {
// 同步
rocketMQTemplate.syncSend("bootTestTopic", "我是boot的一个消息");
// 异步
rocketMQTemplate.asyncSend("bootAsyncTestTopic", "我是boot的一个异步消息", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("失败" + throwable.getMessage());
}
});
// 单向
rocketMQTemplate.sendOneWay("bootOnewayTopic", "单向消息");
// 延迟
Message<String> msg = MessageBuilder.withPayload("我是一个延迟消息").build();
rocketMQTemplate.syncSend("bootMsTopic", msg, 3000, 3);
// 顺序消息 发送者放 需要将一组消息 都发在同一个队列中去 消费者 需要单线程消费
List<MsgModel> msgModels = Arrays.asList(
new MsgModel("qwer", 1, "下单"),
new MsgModel("qwer", 1, "短信"),
new MsgModel("qwer", 1, "物流"),
new MsgModel("zxcv", 2, "下单"),
new MsgModel("zxcv", 2, "短信"),
new MsgModel("zxcv", 2, "物流")
);
msgModels.forEach(msgModel -> {
// 发送 一般都是以json的方式进行处理
rocketMQTemplate.syncSendOrderly("bootOrderlyTopic", JSON.toJSONString(msgModel), msgModel.getOrderSn());
});
}
@Test
void tagKeyTest() throws Exception {
// topic:tag
// rocketMQTemplate.syncSend("bootTagTopic:tagA", "我是一个带tag的消息");
// key是写带在消息头的
Message<String> message = MessageBuilder.withPayload("我是一个带key的消息")
.setHeader(RocketMQHeaders.KEYS, "qwertasdafg")
.build();
rocketMQTemplate.syncSend("bootKeyTopic", message);
}
19.3消息消费
@Component
@RocketMQMessageListener(topic = "bootTestTopic", consumerGroup = "boot-test-consumer-group")
public class ABootSimpleMsgListener implements RocketMQListener<MessageExt> {
/**
* 这个方法就是消费者的方法
* 如果泛型制定了固定的类型 那么消息体就是我们的参数
* MessageExt 类型是消息的所有内容
* ------------------------
* 没有报错 就签收了
* 如果报错了 就是拒收 就会重试
*
* @param message
*/
@Override
public void onMessage(MessageExt message) {
System.out.println(new String(message.getBody()));
}
}
@Component
@RocketMQMessageListener(topic = "bootOrderlyTopic",
consumerGroup = "boot-orderly-consumer-group",
consumeMode = ConsumeMode.ORDERLY, // 顺序消费模式 单线程
maxReconsumeTimes = 5 // 消费重试的次数
)
public class BOrderlyMsgListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
MsgModel msgModel = JSON.parseObject(new String(message.getBody()), MsgModel.class);
System.out.println(msgModel);
}
}
@Component
@RocketMQMessageListener(topic = "bootTagTopic",
consumerGroup = "boot-tag-consumer-group",
selectorType = SelectorType.TAG,// tag过滤模式
selectorExpression = "tagA || tagB"
// selectorType = SelectorType.SQL92,// sql92过滤模式
// selectorExpression = "a in (3,5,7)" // broker.conf中开启enbalePropertyFilter=true
)
public class CTagMsgListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
System.out.println(new String(message.getBody()));
}
}
20.RocketMQ集成SpringBoot消息消费两种模式
Rocketmq消息消费的模式分为两种:负载均衡模式和广播模式
负载均衡模式表示多个消费者交替消费同一个主题里面的消息
广播模式表示每个每个消费者都消费一遍订阅的主题的消息
20.1消息发送
///////////// 测试消息消费模式 集群模块 广播模式
@Test
void modeTest() throws Exception {
for (int i = 1; i <= 5; i++) {
rocketMQTemplate.syncSend("modeTopic", "我是第" + i + "个消息");
}
}
20.2消息消费
/**
* @Author: DLJD
* @Date: 2023/4/22
* [CLUSTERING] 集群模式下 队列会被消费者分摊, 队列数量>=消费者数量 消息的消费位点 mq服务器会记录处理
* BROADCASTING 广播模式下 消息会被每一个消费者都处理一次, mq服务器不会记录消费点位,也不会重试
*/
@Component
@RocketMQMessageListener(topic = "modeTopic",
consumerGroup = "mode-consumer-group-a",
messageModel = MessageModel.CLUSTERING, // 集群模式 负载均衡
consumeThreadNumber = 40
)
public class DC1 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-a组的第一个消费者:" + message);
}
}
/**
* @Author: DLJD
* @Date: 2023/4/22
*/
@Component
@RocketMQMessageListener(topic = "modeTopic",
consumerGroup = "mode-consumer-group-a",
messageModel = MessageModel.CLUSTERING // 集群模式
)
public class DC2 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-a组的第二个消费者:" + message);
}
}
/**
* @Author: DLJD
* @Date: 2023/4/22
*/
@Component
@RocketMQMessageListener(topic = "modeTopic",
consumerGroup = "mode-consumer-group-a",
messageModel = MessageModel.CLUSTERING // 集群模式
)
public class DC3 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-a组的第三个消费者:" + message);
}
}
/**
* @Author: DLJD
* @Date: 2023/4/22
*/
@Component
@RocketMQMessageListener(topic = "modeTopic",
consumerGroup = "mode-consumer-group-b",
messageModel = MessageModel.BROADCASTING // 广播模式
)
public class DC4 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-b组的第一个消费者:" + message);
}
}
/**
* @Author: DLJD
* @Date: 2023/4/22
*/
@Component
@RocketMQMessageListener(topic = "modeTopic",
consumerGroup = "mode-consumer-group-b",
messageModel = MessageModel.BROADCASTING // 广播模式
)
public class DC5 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-b组的第二个消费者:" + message);
}
}
/**
* @Author: DLJD
* @Date: 2023/4/22
*/
@Component
@RocketMQMessageListener(topic = "modeTopic",
consumerGroup = "mode-consumer-group-b",
messageModel = MessageModel.BROADCASTING // 广播模式
)
public class DC6 implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println("我是mode-consumer-group-b组的第三个消费者:" + message);
}
}
1. 生产太快了
生产方可以做业务限流
增加消费者数量,但是消费者数量<=队列数量,适当的设置最大的消费线程数量(根据IO(2n)/CPU(n+1)) 2n逻辑处理器显示数值
动态扩容队列数量,从而增加消费者数量
2. 消费者消费出现问题
排查消费者程序的问题
21.如何确保消息不丢失
①生产者使用同步发送模式 ,收到mq的返回确认以后 顺便往自己的数据库里面写msgId status(0) time
②消费者消费以后 修改数据这条消息的状态 = 1
③写一个定时任务 间隔两天去查询数据 如果有status = 0 and time < day-2
④将mq的刷盘机制设置为同步刷盘
⑤使用集群模式 ,搞主备模式,将消息持久化在不同的硬件上
⑥可以开启mq的trace机制,消息跟踪机制
1.在broker.conf中开启消息追踪
traceTopicEnable=true
2.重启broker即可
3.生产者配置文件开启消息轨迹springboot配置文件
enable-msg-trace: true
4.消费者开启消息轨迹功能,可以给单独的某一个消费者开启
enableMsgTrace = true
在rocketmq的面板中可以查看消息轨迹
默认会将消息轨迹的数据存在 RMQ_SYS_TRACE_TOPIC 主题里面
22.安全
不让别人使用roketmq的控制面板
- 开启acl的控制 在broker.conf中开启aclEnable=true
- 配置账号密码 修改plain_acl.yml
- 修改控制面板的配置文件 放开52/53行 把49行改为true 上传到服务器的jar包平级目录下即可
23.秒杀
23.1架构图
23.2数据库准备
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`price` decimal(10, 2) NULL DEFAULT NULL,
`stocks` int(255) NULL DEFAULT NULL,
`status` int(255) NULL DEFAULT NULL,
`pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, '小米12s', 4999.00, 1000, 2, 'xxxxxx', '2023-02-23 11:35:56', '2023-02-23 16:53:34');
INSERT INTO `goods` VALUES (2, '华为mate50', 6999.00, 10, 2, 'xxxx', '2023-02-23 11:35:56', '2023-02-23 11:35:56');
INSERT INTO `goods` VALUES (3, '锤子pro2', 1999.00, 100, 1, NULL, '2023-02-23 11:35:56', '2023-02-23 11:35:56');
-- ----------------------------
-- Table structure for order_records
-- ----------------------------
DROP TABLE IF EXISTS `order_records`;
CREATE TABLE `order_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NULL DEFAULT NULL,
`order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`goods_id` int(11) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
23.3 e-seckill-web
①pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
②配置文件
server:
port: 8081
tomcat:
threads:
max: 400
spring:
redis:
host: localhost
port: 6379
database: 0
rocketmq:
name-server: 192.168.206.186:9876
producer:
group: seckill-producer-group
access-key: rocketmq2
secret-key: 12345678
③controller配置
@RestController
public class SeckillController {
@Autowired
private StringRedisTemplate redisTemplate;
@Resource
private RocketMQTemplate rocketMQTemplate;
//CAS java无锁的 原子性 安全的
AtomicInteger userIdAt = new AtomicInteger(0);
/**
* 1.用户去重
* 2.库存的预扣减
* 3.消息放入mq
* 秒杀不是一个单独的系统
* 都是大项目的某一个小的功能模块
*
* @param goodsId
* @param userId 真实的项目中 要做登录的 不要穿这个参数
* @return
*/
@GetMapping("seckill")
public String doSecKill(Integer goodsId /*, Integer userId*/) {
// log 2023-4-24 16:58:11
// log 2023-4-24 16:58:11
int userId = userIdAt.incrementAndGet();
// uk uniqueKey = [yyyyMMdd] + userId + goodsId
String uk = userId + "-" + goodsId;
// setIfAbsent = setnx
Boolean flag = redisTemplate.opsForValue().setIfAbsent("uk:" + uk, "");
if (!flag) {
return "您已经参与过该商品的抢购,请参与其他商品O(∩_∩)O~";
}
// 记住 先查再改 再更新 不安全的操作
Long count = redisTemplate.opsForValue().decrement("goodsId:" + goodsId);
if (count < 0) {
// 保证我的redis的库存 最小值是0
redisTemplate.opsForValue().increment("goodsId:" + goodsId);
return "该商品已经被抢完,下次早点来(●ˇ∀ˇ●)";
}
// 方mq 异步处理
rocketMQTemplate.asyncSend("seckillTopic3", uk, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("发送失败:" + throwable.getMessage());
System.out.println("用户的id:" + userId + "商品id" + goodsId);
}
});
return "正在拼命抢购中,请稍后去订单中心查看";
}
/**
* 抢一个付费的商品
* 1.先扣减库存 再付费 | 如果不付费 库存需要回滚
* 2.先付费 再扣减库存 | 如果库存不足 则退费
*/
}
23.4 f-seckill-service
①pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
②.配置文件
server:
port: 8082
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/seckill?serverTimezone=GMT%2B8&useSSL=false
redis:
host: localhost
port: 6379
database: 0
rocketmq:
name-server: 192.168.206.186:9876
consumer:
access-key: rocketmq2
secret-key: 12345678
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
③逆向生成实体类等
④修改启动类
@SpringBootApplication
@MapperScan(basePackages = {"com.powernode.mapper"})
public class SpikeServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SpikeServiceApplication.class, args);
}
⑤同步mysql数据到redis
List<Goods> selectSeckillGoods();
<!-- 查询数据库中需要参于秒杀的商品数据 status = 2 -->
<select id="selectSeckillGoods" resultMap="BaseResultMap">
select `id`,`stocks` from goods where `status` = 2
</select>
@Component
public class DataSync {
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private StringRedisTemplate redisTemplate;
// @Scheduled(cron = "0 0 10 0 0 ?")
// public void initData(){
// }
/**
* 我希望这个方法再项目启动以后
* 并且再这个类的属性注入完毕以后执行
* bean生命周期了
* 实例化 new
* 属性赋值
* 初始化 (前PostConstruct/中InitializingBean/后BeanPostProcessor)
* 使用
* 销毁
* ----------
* 定位不一样
*/
@PostConstruct
public void initData() {
List<Goods> goodsList = goodsMapper.selectSeckillGoods();
if (CollectionUtils.isEmpty(goodsList)) {
return;
}
goodsList.forEach(goods -> {
redisTemplate.opsForValue().set("goodsId:" + goods.getGoodsId(), goods.getTotalStocks().toString());
});
}
}
⑥创建秒杀监听
@Component
@RocketMQMessageListener(topic = "seckillTopic3",
consumerGroup = "seckill-consumer-group3",
consumeMode = ConsumeMode.CONCURRENTLY,
consumeThreadNumber = 40
)
public class SeckillListener implements RocketMQListener<MessageExt> {
@Autowired
private GoodsService goodsService;
@Autowired
private StringRedisTemplate redisTemplate;
int ZX_TIME = 20000;
/**
* 扣减库存
* 写订单表
*
* @param message
*/
// @Override
// public void onMessage(MessageExt message) {
// String msg = new String(message.getBody());
// // userId + "-" + goodsId
// Integer userId = Integer.parseInt(msg.split("-")[0]);
// Integer goodsId = Integer.parseInt(msg.split("-")[1]);
// // 方案一: 再事务外面加锁 可以实现安全 没法集群
// jvm EntrySet WaitSet
// synchronized (this) {
// goodsService.realSeckill(userId, goodsId);
// }
// }
// 方案二 分布式锁 mysql(行锁) 不适合并发较大场景
// @Override
// public void onMessage(MessageExt message) {
// String msg = new String(message.getBody());
// // userId + "-" + goodsId
// Integer userId = Integer.parseInt(msg.split("-")[0]);
// Integer goodsId = Integer.parseInt(msg.split("-")[1]);
// goodsService.realSeckill(userId, goodsId);
// }
// 方案三: redis setnx 分布式锁 压力会分摊到redis和程序中执行 缓解db的压力
@Override
public void onMessage(MessageExt message) {
String msg = new String(message.getBody());
Integer userId = Integer.parseInt(msg.split("-")[0]);
Integer goodsId = Integer.parseInt(msg.split("-")[1]);
int currentThreadTime = 0;
while (true) {
// 这里给一个key的过期时间,可以避免死锁的发生 Duration.ofSeconds(30)避免在操作数据库的时候卡死
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "", Duration.ofSeconds(30));
if (flag) {
// 拿到锁成功 return 结束这个线程
try {
goodsService.realSeckill(userId, goodsId);
return;
} finally {
// 删除
redisTemplate.delete("lock:" + goodsId);
}
} else {
//false的线程需要等待正在处理减库存的线程释放锁
currentThreadTime += 200;
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
controller
@Service
public class GoodsServiceImpl implements GoodsService {
@Resource
private GoodsMapper goodsMapper;
@Autowired
private OrderMapper orderMapper;
@Override
public int deleteByPrimaryKey(Integer goodsId) {
return goodsMapper.deleteByPrimaryKey(goodsId);
}
@Override
public int insert(Goods record) {
return goodsMapper.insert(record);
}
@Override
public int insertSelective(Goods record) {
return goodsMapper.insertSelective(record);
}
@Override
public Goods selectByPrimaryKey(Integer goodsId) {
return goodsMapper.selectByPrimaryKey(goodsId);
}
@Override
public int updateByPrimaryKeySelective(Goods record) {
return goodsMapper.updateByPrimaryKeySelective(record);
}
@Override
public int updateByPrimaryKey(Goods record) {
return goodsMapper.updateByPrimaryKey(record);
}
/////////////////////////////////
/**
*
* 常规的 方案
* 锁加载调用方法的地方 要加载事务外面 注意事项:先释放锁,在提交事务 A释放锁后没有提交事务,B就进来了
* @param userId
* @param goodsId
*/
// @Override
// @Transactional(rollbackFor = Exception.class) // rr
// public void realSeckill(Integer userId, Integer goodsId) {
// // 扣减库存 插入订单表
// Goods goods = goodsMapper.selectByPrimaryKey(goodsId);
// int finalStock = goods.getTotalStocks() - 1;
// if (finalStock < 0) {
// // 只是记录日志 让代码停下来 这里的异常用户无法感知
// throw new RuntimeException("库存不足:" + goodsId);
// }
// goods.setTotalStocks(finalStock);
// goods.setUpdateTime(new Date());
// // update goods set stocks = 1 where id = 1 没有行锁
// int i = goodsMapper.updateByPrimaryKey(goods);
// if (i > 0) {
// Order order = new Order();
// order.setGoodsid(goodsId);
// order.setUserid(userId);
// order.setCreatetime(new Date());
// orderMapper.insert(order);
// }
// }//
/**
* 行锁(innodb)方案 mysql 不适合用于并发量特别大的场景
* 因为压力最终都在数据库承担
*
* @param userId
* @param goodsId
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void realSeckill(Integer userId, Integer goodsId) {
// update goods set total_stocks = total_stocks - 1 where goods_id = goodsId and total_stocks - 1 >= 0;
// 通过mysql来控制锁
int i = goodsMapper.updateStock(goodsId);
if (i > 0) {
Order order = new Order();
order.setGoodsid(goodsId);
order.setUserid(userId);
order.setCreatetime(new Date());
orderMapper.insert(order);
}
}
}