java实现生产者消费者模式
BlockingQueue(阻塞队列)是java中常见的容器,在多线程编程中被广泛使用。
- 当队列容器已满时生产者线程被阻塞,直到队列未满后才可以继续put;
- 当队列容器为空时,消费者线程被阻塞,直至队列非空时才可以继续take。
(1)实体类(口罩):
public class KouZhao {
private Integer id;
private String type;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return "KouZhao{" +
"id=" + id +
", type='" + type + '\'' +
'}';
}
}
(2).生产者Producer.java:
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable {
private BlockingQueue<KouZhao> queue;
public Producer(BlockingQueue<KouZhao> queue) {
this.queue = queue;
}
private Integer index = 0;
@Override
public void run() {
while (true) {
try {
Thread.sleep(200);
if (queue.remainingCapacity() <= 0) {
System.out.println("口罩已经堆积如山了,大家快来买。。。");
} else {
KouZhao kouZhao = new KouZhao();
kouZhao.setType("N95");
kouZhao.setId(index++);
System.out.println("正在生产第" + (index - 1) + "号口罩。。。");
queue.put(kouZhao);
System.out.println("已经生产了口罩:" + queue.size() + "个");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(3)消费者Consumer.java:
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable {
private BlockingQueue<KouZhao> queue;
public Consumer(BlockingQueue<KouZhao> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
System.out.println("正在准备买口罩。。。");
final KouZhao kouZhao = queue.take();
System.out.println("买到了口罩:" + kouZhao.getId()
+ " " + kouZhao.getType() + " 口罩");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(4).程序启动类:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class App {
public static void main(String[] args) {
BlockingQueue<KouZhao> queue = new ArrayBlockingQueue<>(20);
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
上述代码放到生产环境显然是不行的,比如没有集群,没有分布式,太单一,没有考虑过限流,消息有没有持久化,无法确定消息一定能发送成功…
消息中间件
消息中间件也可以称消息队列,是指用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息队列模型,可以在分布式环境下扩展进程的通信。
异步处理、流量削峰、限流、缓冲、排队、最终一致性、消息驱动等需求的场景都可以使用消息中间件。
主流消息中间件
当前业界比较流行的开源消息中间件包括:ActiveMQ、RabbitMQ、RocketMQ、Kafka、ZeroMQ等,其中应用最为广泛的要数RabbitMQ、RocketMQ、Kafka这三款。
Redis在某种程度上也可以是实现类似“Queue”和“Pub/Sub”的机制,严格意义上不算消息中间件。
RabbitMQ开始是用在电信业务的可靠通信的,也是少有的几款支持AMQP协议的产品之一。
优点:
- 轻量级,快速,部署使用方便
- 支持灵活的路由配置。RabbitMQ中,在生产者和队列之间有一个交换器模块。根据配置的路由规则,生产者发送的消息可以发送到不同的队列中。路由规则很灵活,还可以自己实现。
- RabbitMQ的客户端支持大多数的编程语言。
缺点:
- 如果有大量消息堆积在队列中,性能会急剧下降
- RabbitMQ的性能在Kafka和RocketMQ中是最差的,每秒处理几万到几十万的消息。如果应用要求高的性能,不要选择RabbitMQ。
- RabbitMQ是Erlang开发的,功能扩展和二次开发代价很高。
消息中间件应用场景
消息中间件的使用场景非常广泛,比如,12306购票的排队锁座,电商秒杀,大数据实时计算等。
- 削去秒杀场景下的峰值写流量将秒杀请求暂存于消息队列,业务服务器响应用户“秒杀结果正在处理中。。。”,释放系统资源去处理其它用户的请求。
- 先处理主要的业务,异步处理次要的业务。次要流程比如购买成功之后会给用户发优惠券,增加用户的积分。
JMS规范和AMQP协议
JMS规范
JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM,Message oriented Middleware)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。与具体平台无关的API,绝大多数MOM提供商都支持。
它类似于JDBC(Java Database Connectivity)。
JMS模式
Java消息服务应用程序结构支持两种模式:1. 点对点也叫队列模式 2. 发布/订阅模式
点对点或队列模型下:
一个生产者向一个特定的队列发布消息,一个消费者从该队列中读取消息。这里,生产者知道消费者的队列,并直接将消息发送到消费者的队列,概括为:
- 一条消息只有一个消费者获得生产者无需在接收者消费该消息期间处于运行状态,接收者也同样无需在消息发送时处于运行状态。
- 每一个成功处理的消息要么自动确认,要么由接收者手动确认。
发布/订阅模式:
- 支持向一个特定的主题发布消息。
- 0或多个订阅者可能对接收特定消息主题的消息感兴趣。
- 发布者和订阅者彼此不知道对方。
- 多个消费者可以获得消息
AMQP协议
AMQP全称高级消息队列协议(Advanced Message Queuing Protocol),是一种标准,类似于JMS,兼容JMS协议。目前RabbitMQ主流支持AMQP 0-9-1,3.8.4版本支持AMQP 1.0。
Publisher:消息发送者,将消息发送到Exchange并指定RoutingKey,以便queue可以接收到指定的消息。
Consumer:消息消费者,从queue获取消息,一个Consumer可以订阅多个queue以从多个queue中接收消息。
Server:一个具体的MQ服务实例,也称为Broker。
Virtual host:虚拟主机,一个Server下可以有多个虚拟主机,用于隔离不同项目,一个Virtualhost通常包含多个Exchange、Message Queue。
Exchange:交换器,接收Producer发送来的消息,把消息转发到对应的Message Queue中。
Routing key:路由键,用于指定消息路由规则(Exchange将消息路由到具体的queue中),通常需要和具体的Exchange类型、Binding的Routing key结合起来使用。
Bindings:指定了Exchange和Queue之间的绑定关系。Exchange根据消息的Routing key和Binding配置(绑定关系、Binding、Routing key等)来决定把消息分派到哪些具体的queue中。这依赖于Exchange类型。
Message Queue:实际存储消息的容器,并把消息传递给最终的Consumer。
AMQP 使用的数据类型如下:
- Integers(数值范围1-8的十进制数字):用于表示大小,数量,限制等,整数类型无符号的,可以在帧内不对齐。
- Bits(统一为8个字节):用于表示开/关值。
- Short strings:用于保存简短的文本属性,字符串个数限制为255,8个字节
- Long strings:用于保存二进制数据块。
- Field tables:包含键值对,字段值一般为字符串,整数等。
RabbitMQ实战
RabbitMQ,俗称“兔子MQ”(可见其轻巧,敏捷),是目前非常热门的一款开源消息中间件,不管是互联网行业还是传统行业都广泛使用(最早是为了解决电信行业系统之间的可靠通信而设计)。
- 高可靠性、易扩展、高可用、功能丰富等
- 支持大多数(甚至冷门)的编程语言客户端。
- RabbitMQ遵循AMQP协议,自身采用Erlang(一种由爱立信开发的通用面向并发编程的语言)编写。
- RabbitMQ也支持MQTT等其他协议。
RabbitMQ Exchange类型
RabbitMQ常用的交换器类型有:fanout、direct、topic、headers四种。
Fanout:会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中
Direct:direct类型的交换器路由规则很简单,它会把消息路由到那些BindingKey和RoutingKey完全匹配的队列中
Topic:
topic类型的交换器在direct匹配规则上进行了扩展,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,这里的匹配规则稍微不同,它约定:
BindingKey和RoutingKey一样都是由"."分隔的字符串;BindingKey中可以存在两种特殊字符“”和“#”,用于模糊匹配,其中"“用于匹配一个单词,”#"用于匹配多个单词(可以是0个)。
Headers:headers类型的交换器不依赖于路由键的匹配规则来路由信息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送的消息到交换器时,RabbitMQ会获取到该消息的headers,对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果匹配,消息就会路由到该队列。headers类型的交换器性能很差,不实用。
SpringBoot整合RabbitMQ
- 添加starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- application.properties中添加连接信息:
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.virtual-host=/
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
spring.rabbitmq.port=5672
- RabbitConfig类
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
@Bean
public Queue queue() {
return new Queue("queue.boot", false, false, false, null);
}
//交换器名称,交换器类型(),是否是持久化的,是否自动删除,交换器属性Map集合
@Bean
public Exchange exchange() {
return new TopicExchange("ex.boot", false, false, null);
}
// 绑定的目的地,绑定的类型:到交换器还是到队列,交换器名称,路由key,绑定的属性
@Bean
public Binding binding() {
return new Binding("queue.boot",
Binding.DestinationType.QUEUE,
"ex.boot",
"key.boot",
null);
}
}
或者:
@Configuration
public class RabbitMQConfig {
// 自定义消息转换器
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
// 1.定义fanout类型的交换器
@Bean
public Exchange fanout_exchange(){
return ExchangeBuilder.fanoutExchange(MQConstants.FANOUT_EXCHANGE).build();
}
// 2.定义两个名称不同的消息队列
@Bean
public Queue fanout_queue_email(){
return new Queue(MQConstants.FANOUT_QUEUE_EMAIL);
}
@Bean
public Queue fanout_queue_sms(){
return new Queue(MQConstants.FANOUT_QUEUE_SMS);
}
//3.将两个名称不同的消息队列于交换器绑定
@Bean
public Binding bindingEmail() {
//fanout广播类型的交换器不需要指定RoutingKey
return BindingBuilder.bind(fanout_queue_email()).to(fanout_exchange()).with("").noargs();
}
@Bean
public Binding bindingSms() {
return BindingBuilder.bind(fanout_queue_sms()).to(fanout_exchange()).with("").noargs();
}
}
- 使用RestController发送消息
@RestController
public class MessageController {
@Autowired
private AmqpTemplate rabbitTemplate;
@RequestMapping("/rabbit/{message}")
public String receive(@PathVariable String message) throws UnsupportedEncodingException {
final MessageProperties messageProperties = MessagePropertiesBuilder.newInstance().setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
.setContentEncoding("utf-8")
.setHeader("hello", "world")
.build();
final Message msg = MessageBuilder
.withBody(message.getBytes("utf-8"))
.andProperties(messageProperties)
.build();
rabbitTemplate.send("ex.boot", "key.boot", msg);
return "ok";
}
}
- 使用监听器,用于推消息
@Configuration
public class RabbitConfig {
// 监听哪个对列的名称
@Bean
public Queue queue() {
return QueueBuilder.nonDurable("queue.boot").build();
}
}
@Component
public class MyMessageListener {
// @RabbitListener(queues = "queue.boot")
// public void getMyMessage(@Payload String message, @Header(name = "hello") String value, Channel channel) {
// System.out.println(message);
// System.out.println("hello = " + value);
//
// // 确认消息
// channel.basicAck();
// // 拒收消息
// channel.basicReject();
// }
private Integer index = 0;
@RabbitListener(queues = "queue.boot")
public void getMyMessage(Message message, Channel channel) throws IOException {
String value = message.getMessageProperties().getHeader("hello");
System.out.println(message);
System.out.println("hello = " + value);
final long deliveryTag = message.getMessageProperties().getDeliveryTag();
if (index % 2 == 0) {
// 确认消息
channel.basicAck(deliveryTag, false);
} else {
// 拒收消息
channel.basicReject(deliveryTag, false);
}
index++;
}
}
RabbitMQ高级特性
消息可靠性
可靠性分析
TTL(过期时间)机制
在京东下单,订单创建成功,等待支付,一般会给30分钟的时间,开始倒计时。如果在这段时间内用户没有支付,则默认订单取消。
方案一:定期轮询(数据库等)
用户下单成功,将订单信息放入数据库,同时将支付状态放入数据库,用户付款更改数据库状态。定期轮询数据库支付状态,如果超过30分钟就将该订单取消。
优点:设计实现简单
缺点:需要对数据库进行大量的IO操作,效率低下。
方案二:Timer
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("用户没有付款,交易取消:" + simpleDateFormat.format(new Date(System.currentTimeMillis())));
timer.cancel();
}
};
System.out.println("等待用户付款:" + simpleDateFormat.format(new Date(System.currentTimeMillis())));// 10秒后执行
timerTasktimer.schedule(timerTask, 10 * 1000);
缺点:
Timers没有持久化机制.
Timers不灵活 (只可以设置开始时间和重复间隔,对等待支付貌似够用)
Timers 不能利用线程池,一个timer一个线程
Timers没有真正的管理计划
方案三:ScheduledExecutorService
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");// 线程工厂
ThreadFactory factory = Executors.defaultThreadFactory();// 使用线程池
ScheduledExecutorService service = new ScheduledThreadPoolExecutor(10, factory);
System.out.println("开始等待用户付款10秒:" + format.format(new Date()));
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("用户未付款,交易取消:" + format.format(new Date())); }// 等待10s 单位秒
}, 10, TimeUnit.SECONDS);
优点:可以多线程执行,一定程度上避免任务间互相影响,单个任务异常不影响其它任务。
在高并发的情况下,不建议使用定时任务去做,因为太浪费服务器性能,不建议。
方案四:RabbitMQ
RabbitMQ 可以对消息和队列两个维度来设置TTL。
任何消息中间件的容量和堆积能力都是有限的,如果有一些消息总是不被消费掉,那么需要有一种过期的机制来做兜底。
目前有两种方法可以设置消息的TTL。
- 通过Queue属性设置,队列中所有消息都有相同的过期时间。
- 对消息自身进行单独设置,每条消息的TTL 可以不同。
如果两种方法一起使用,则消息的TTL 以两者之间较小数值为准。通常来讲,消息在队列中的生存时间一旦超过设置的TTL 值时,就会变成“死信”(Dead Message),消费者默认就无法再收到该消息。当然,“死信”也是可以被取出来消费的。
(一).原生API案例
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
public class Producer {
public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, URISyntaxException {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
try (final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel()) {
Map<String, Object> arguments = new HashMap<>();
// 消息队列中消息过期时间,30s
arguments.put("x-message-ttl", 10 * 1000);
// 如果消息队列没有消费者,则10s后消息过期,消息队列也删除
// arguments.put("x-expires", 10 * 1000);
arguments.put("x-expires", 60 * 1000);
channel.queueDeclare("queue.ttl.waiting",
true,
false,
false,
arguments);
channel.exchangeDeclare("ex.ttl.waiting",
"direct",
true,
false,
null);
channel.queueBind("queue.ttl.waiting", "ex.ttl.waiting", "key.ttl.waiting");
final AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.contentEncoding("utf-8")
.deliveryMode(2) // 持久化的消息
.build();
channel.basicPublish("ex.ttl.waiting",
"key.ttl.waiting",
null,
"等待的订单号".getBytes("utf-8"));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
(二.Spring Boot方式)
@Configuration
public class RabbitConfig {
@Bean
public Queue queueTTLWaiting() {
Map<String, Object> props = new HashMap<>();
// 对于该队列中的消息,设置都等待10s
props.put("x-message-ttl", 10000);
Queue queue = new Queue("q.pay.ttl-waiting", false, false, false, props);
return queue;
}
@Bean
public Queue queueWaiting() {
Queue queue = new Queue("q.pay.waiting", false, false, false);
return queue;
}
@Bean
public Exchange exchangeTTLWaiting() {
DirectExchange exchange = new DirectExchange("ex.pay.ttl-waiting", false, false);
return exchange;
}
@Bean
public Exchange exchangeWaiting() {
DirectExchange exchange = new DirectExchange("ex.pay.waiting", false, false);
return exchange;
}
@Bean
public Binding bindingTTLWaiting() {
return BindingBuilder.bind(queueTTLWaiting()).to(exchangeTTLWaiting()).with("pay.ttl-waiting").noargs();
}
@Bean
public Binding bindingWaiting() {
return BindingBuilder.bind(queueWaiting()).to(exchangeWaiting()).with("pay.waiting").noargs();
}
}
// 方式一
@RequestMapping("/pay/queuettl")
public String sendMessage() {
rabbitTemplate.convertAndSend("ex.pay.ttl-waiting", "pay.ttl-waiting", "发送了TTL-WAITING-MESSAGE");
return "queue-ttl-ok";
}
// 方式二
@RequestMapping("/pay/msgttl")
public String sendTTLMessage() throws UnsupportedEncodingException {
MessageProperties properties = new MessageProperties();
properties.setExpiration("5000");
Message message = new Message("发送了WAITING-MESSAGE".getBytes("utf-8"), properties);
rabbitTemplate.convertAndSend("ex.pay.waiting", "pay.waiting", message);
return "msg-ttl-ok";
}
死信队列
DLX,全称为Dead-Letter-Exchange,死信交换器。消息在一个队列中变成死信(Dead Letter)之后,被重新发送到一个特殊的交换器(DLX)中,同时,绑定DLX的队列就称为“死信队列”。
以下几种情况导致消息变为死信:
- 消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false;
- 消息过期;
- 队列达到最大长度。
(一).原生api实现方式
public class Producer {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://root:123456@node1:5672/%2f");
try (final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel()) {
// 正常业务的交换器
channel.exchangeDeclare("ex.biz", "direct", true);
// 声明死信交换器 DLX
channel.exchangeDeclare("ex.dlx", "direct", true);
// 声明队列做死信队列
channel.queueDeclare("queue.dlx", true, false, false, null);
// 绑定死信交换器和死信队列
channel.queueBind("queue.dlx", "ex.dlx", "key.dlx");
Map<String, Object> arguments = new HashMap<>();
// 指定消息队列中的消息过期时间
arguments.put("x-message-ttl", 10000);
// 指定过期消息通过死信交换器发送到死信队列,死信交换器的名称,DLX
arguments.put("x-dead-letter-exchange", "ex.dlx");
// 指定死信交换器的路由键
arguments.put("x-dead-letter-routing-key", "key.dlx");
channel.queueDeclare("queue.biz", true, false, false, arguments);
// 绑定业务的交换器和消息队列
channel.queueBind("queue.biz", "ex.biz", "key.biz");
channel.basicPublish("ex.biz", "key.biz", null, "orderid.45789987678".getBytes());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
(二).Spring Boot实现方式:
@Autowired
private AmqpTemplate rabbitTemplate;
@RequestMapping("/go")
public String distributeGo() {
rabbitTemplate.convertAndSend("ex.go", "go", "送单到石景山x小区,请在10秒内接受任务");
return "任务已经下发,等待送单。。。";
}
@RequestMapping("/notgo")
public String getAccumulatedTask() {
String notGo = (String) rabbitTemplate.receiveAndConvert("q.go.dlx");
return notGo;
}
延迟队列