概要
- 类继承关系
|
|
- 定义
|
|
TreeMap
实现NavigableMap
接口,说明支持一系列的导航方法
- 核心成员变量
|
- 内部节点
|
- 要点
]
|
|
|
|
TreeMap
实现NavigableMap
接口,说明支持一系列的导航方法
|
|
]
生产者与消费者相互解耦,怎么保证生产者已将消息投递到 RabbitMQ
服务端,又如何确认消费者已经消费了该消息?
RabbitMQ
为我们提供了两种方式:
AMQP
事务机制实现,这也是AMQP
协议层面提供的解决方案;channel
设置成confirm
模式来实现;RabbitMQ
事务机制注意:使用事务模式会导致服务端吞吐量急剧下降,实际使用场景很少,这里简单介绍一下
RabbitMQ
中与事务机制有关的方法有三个:txSelect()
, txCommit()
以及txRollback()
, txSelect
用于将当前channel
设置成transaction
模式(通道事务),txCommit
用于提交事务,txRollback
用于回滚事务,在通过txSelect
开启事务之后,我们便可以发布消息给broker
代理服务器了,如果txCommit
提交成功了,则消息一定到达了broker
了,如果在txCommit
执行之前broker
异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback
回滚事务了。
|
|
模仿了协议中已经存在的消费者ACK
确认机制,生产者将信道设置成confirm
模式,一旦信道进入confirm
模式,所有在该信道上面发布的消息都会被指派一个唯一的ID,一旦消息被投递到所有匹配的队列之后,broker
就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出
confirm
模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息
|
|
Spring Amqp配置
|
|
当mandatory
标志设置为true时,如果exchange
根据自身类型和消息routingKey
无法找到一个合适的queue
存储消息,那么broker
会调用basic.return
方法将消息返还给生产者;当mandatory
设置为false时,出现上述情况broker会直接将消息丢弃
confirmCallback
回调方法
|
|
returnCallback
回调方法
|
|
注意:
confirm
主要是用来判断消息是否有正确到达交换机,如果有,那么就 ack 就返回 true;如果没有,则是 false。return
则表示如果你的消息已经正确到达交换机,但是后续处理出错了,那么就会回调 return,并且把信息送回给你(前提是需要设置了 Mandatory,不设置那么就丢弃);如果消息没有到达交换机,那么不会调用 return 的东西。为了保证消息从队列可靠地到达 Consumer
,RabbitMQ
提供消息确认机制。消费者在声明队列时,可以指定noAck
参数,当noAck=false
时,RabbitMQ
会等待消费者显式发回ack
信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ
会在队列中消息被消费后立即删除它
消息生产、消费的流程图:
ack
模式NONE
该模式下, 当 broker
发送消息成功后, 会立即将此消息从消息队列中删除, 而不会等待消费者的 ACK
回复
MANUAL
AUTO
(默认模式)
当 broker
发送消息给消费者时, 不会立即将此消息删除, 而是需要等待消费者的 ACK 回复后才会删除消息. 因此在手动 ACK
模式下, 当消费者收到消息并处理完成后, 需要向 broker
显示地发送 ACK 指令.
|
|
Java
虚拟机所暴露的线程状态,与操作系统底层的线程状态是两个不同层面的事
JVM
中定义的线程状态有以下几种:
|
|
blocked
和waiting
状态的区别blocked
是虚拟机认为程序还不能进入某个区域,因为同时进去就会有问题,这是一块临界区wait
操作的先决条件是要进入临界区,也就是线程已经拿到锁了,自己可能进去做了一些事情,但此时通过判定业务上的参数,发现还有一些其他配合的资源没有准备充分,那么自己就等等再做其他事情。sleep()
和wait()
操作的区别sleep
是Thread
类的静态方法,wait
是Object
类中定义的方法Thread.sleep
不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep
不会让线程释放锁;Object.wait
会释放锁,进入等待队列中wait
不带计时参数是WAITING
状态,带计时参数是TIMED_WAITING
状态;不管是通过notify()
唤醒还是超时时间已到,都需要重新获取锁才能继续执行
|
|
start()
方法是同步的,而且执行前会判断当前线程状态是否为NEW
;也就是说,同一个线程只能执行一次
|
|
使用jstack
查看线程状态:
|
|
可见JVM线程状态为RUNNABLE
;I/O 阻塞也是如此
注意:进行传统上的 IO 操作时,我们也会说“阻塞”,但这个“阻塞”与线程的BLOCKED
状态是两码事
|
|
项目中为了加快响应时间以及减轻数据库压力,使用了redis
缓存,同时缓存设置了过期时间(1天)
|
|
一般数据都存放在关系型数据库中,以常用的MySQL数据库为例,正常情况下响应时间在10ms以内甚至更短;但是当数据上亿条,任何一款关系型数据库的响应时间都不可能控制在10ms以内
同时高并发情况下,比如同时来1万次请求,MySQL单库TPS(每秒事务量)大概只有1500左右,其它的请求只能处于等待状态,严重情况下数据库崩溃
对于缓存来说,数据变更少且查询比较频繁是最好的场景,如果查询量不够大或者数据变动太频繁,缓存也就是失去了意义。
有些数据量不常变化,但是访问十分频繁,例如省、市地区数据。针对这种场景,可以将数据加载到应用的内存中,以提升系统的访问效率,减少数据库访问同时加快响应时间。
比较常见的本地缓存有Guava Cache
,使用方式如下:
|
|
JVM
中,无法共享提到分布式缓存基本上都会说到 Redis
,Redis
使用内存作为存储,所以性能上要比数据库要好很多,再加上Redis
还支持很多种数据结构,使用起来比较方便;但是,Redis
需要通过网络来访问,所以网络的性能决定了 Reids
的瓶颈
更多策略见缓存更新的套路,这里具体分析喜马拉雅的缓存更新策略:先更新数据库,再失效缓存
|
|
这种策略也会产生问题,比如:
|
|
但是,这种情况出现的概率非常低。这个场景需要发生在读缓存时缓存失效,并发着有一个写操作。而实际上数据库的写操作比读操作慢得多,而且还要锁表,而读操作需要在写操作之前进入数据库操作,又要在写操作完成后更新缓存,所有的这些条件都具备的概率并不大。所以,Cache Aside Pattern 还是相对靠谱的方式。
|
|
requeue-rejected
设为false表示一条消息即使没有被ack,也不会再重新发送(默认是会重新加入队列)RabbitMQ
可以针对Queue
和Message
设置x-message-tt
,来控制消息的生存时间,如果超时,则消息变为dead letter