这三年被分布式坑惨了,曝光十大坑
Posted 悟空聊架构
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这三年被分布式坑惨了,曝光十大坑相关的知识,希望对你有一定的参考价值。
节点挂了。
Leader 选举机制
,如果某个节点挂了,会从 follower
中重新选举一个 leader 出来。(leader 作为写数据的入口,follower 作为读的入口)那多重影分身之术
有什么缺点?
Base
理论,这里给不知道的同学做一个扫盲。是 Basically Available
(基本可用)、Soft state
(软状态)和 Eventually consistent
(最终一致性)三个短语的缩写。BASE
理论是对 CAP
中 AP
的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足 BASE
理论的事务,我们称之为柔性事务
。消息队列中间件都有可能出现消息重复消费问题。这种问题并不是 MQ 自己保证的,而是需要开发人员来保证。这几款消息队列中间都是是全球最牛的分布式消息队列,那肯定考虑到了消息的幂等性。我们以 Kafka 为例,看看 Kafka 是怎么保证消息队列的幂等性。
Kafka 有一个 偏移量
的概念,代表着消息的序号,每条消息写到消息队列都会有一个偏移量,消费者消费了数据之后,每过一段固定的时间,就会把消费过的消息的偏移量提交一下,表示已经消费过了,下次消费就从偏移量后面开始消费。
坑:
当消费完消息后,还没来得及提交偏移量,系统就被关机了,那么未提交偏移量的消息则会再次被消费。
如下图所示,队列中的数据 A、B、C,对应的偏移量分别为 100、101、102,都被消费者消费了,但是只有数据 A 的偏移量 100 提交成功,另外 2 个偏移量因系统重启而导致未及时提交。
重启后,消费者又是拿偏移量 100 以后的数据,从偏移量 101 开始拿消息。所以数据 B 和数据 C 被重复消息。
如下图所示:
操作天然幂等性,所以不用考虑 Redis 写数据的问题。其他场景方案 生产者发送每条数据时,增加一个全局唯一 id,类似订单 id。每次消费时,先去 Redis 查下是否有这个 id,如果没有,则进行正常处理消息,且将 id 存到 Redis。如果查到有这个 id,说明之前消费过,则不要进行重复处理这条消息。 不同业务场景,可能会有不同的幂等性方案,大家选择合适的即可,上面的几种方案只是提供常见的解决思路。 坑:
消息丢失会带来什么问题?如果是订单下单、支付结果通知、扣费相关的消息丢失,则可能造成财务损失,如果量很大,就会给甲方带来巨大损失。
那消息队列是否能保证消息不丢失呢?答案:否。主要有三种场景会导致消息丢失。
,如果消息没有进队列,则生产者受到异常报错,并进行回滚 channel.txRollback
,然后重试发送消息;如果收到了消息,则可以提交事务 channel.txCommit
。但这是一个同步的操作,会影响性能。confirm 机制(推荐,异步方式) 我们可以采用另外一种模式:confirm
模式来解决同步机制的性能问题。每次生产者发送的消息都会分配一个唯一的 id,如果写入到了 RabbitMQ 队列中,则 RabbitMQ 会回传一个 ack
消息,说明这个消息接收成功。如果 RabbitMQ 没能处理这个消息,则回调 nack
接口。说明需要重试发送消息。
也可以自定义超时时间 + 消息 id 来实现超时等待后重试机制。但可能出现的问题是调用 ack 接口时失败了,所以会出现消息被发送两次的问题,这个时候就需要保证消费者消费消息的幂等性。
和 confirm
模式的区别:的时候将其设置为持久化。发送消息的时候将消息的 deliveryMode
设置为 2 。 开启生产者 confirm
模式,可以重试发送消息。 给生产者。消费者处理完消息再主动 ack
,告诉消息队列我处理完了。 问题: 那这种主动 ack
有什么漏洞了?如果 主动 ack
的时候挂了,怎么办?
则可能会被再次消费,这个时候就需要幂等处理了。
问题: 如果这条消息一直被重复消费怎么办?
则需要有加上重试次数的监测,如果超过一定次数则将消息丢失,记录到异常表或发送异常通知给值班人员。
的某个 broker(节点)宕机了,重新选举 leader (写入的节点)。如果 leader 挂了,follower 还有些数据未同步完,则 follower 成为 leader 后,消息队列会丢失一部分数据。解决方案
给 topic 设置 replication.factor
参数,值必须大于 1,要求每个 partition 必须有至少 2 个副本。 给 kafka 服务端设置 min.insyc.replicas
必须大于 1,表示一个 leader 至少一个 follower 还跟自己保持联系。 坑:
用户先下单成功,然后取消订单,如果顺序颠倒,则最后数据库里面会有一条下单成功的订单。
RabbitMQ 场景:
生产者向消息队列按照顺序发送了 2 条消息,消息1:增加数据 A,消息2:删除数据 A。 期望结果:数据 A 被删除。 但是如果有两个消费者,消费顺序是:消息2、消息 1。则最后结果是增加了数据 A。 RabbitMQ 解决方案:
将 Queue 进行拆分,创建多个内存 Queue,消息 1 和 消息 2 进入同一个 Queue。 创建多个消费者,每一个消费者对应一个 Queue。 Kafka 场景:
创建了 topic,有 3 个 partition。 创建一条订单记录,订单 id 作为 key,订单相关的消息都丢到同一个 partition 中,同一个生产者创建的消息,顺序是正确的。 为了快速消费消息,会创建多个消费者去处理消息,而为了提高效率,每个消费者可能会创建多个线程来并行的去拿消息及处理消息,处理消息的顺序可能就乱序了。 Kafka 解决方案:
解决方案和 RabbitMQ 类似,利用多个 内存 Queue,每个线程消费 1个 Queue。 具有相同 key 的消息 进同一个 Queue。 消息积压:消息队列里面有很多消息来不及消费。
场景 1: 消费端出了问题,比如消费者都挂了,没有消费者来消费了,导致消息在队列里面不断积压。
场景 2: 消费端出了问题,比如消费者消费的速度太慢了,导致消息不断积压。
坑:比如线上正在做订单活动,下单全部走消息队列,如果消息不断积压,订单都没有下单成功,那么将会损失很多交易。
解决方案:解铃还须系铃人
修复代码层面消费者的问题,确保后续消费速度恢复或尽可能加快消费的速度。 停掉现有的消费者。 临时建立好原先 5 倍的 Queue 数量。 临时建立好原先 5 倍数量的 消费者。 将堆积的消息全部转入临时的 Queue,消费者来消费这些 Queue。 坑:
RabbitMQ 可以设置过期时间,如果消息超过一定的时间还没有被消费,则会被 RabbitMQ 给清理掉。消息就丢失了。
解决方案:
准备好批量重导的程序 手动将消息闲时批量重导 坑:
当消息队列因消息积压导致的队列快写满,所以不能接收更多的消息了。生产者生产的消息将会被丢弃。
解决方案:
判断哪些是无用的消息,RabbitMQ 可以进行 Purge Message
操作。 如果是有用的消息,则需要将消息快速消费,将消息里面的内容转存到数据库。 准备好程序将转存在数据库中的消息再次重导到消息队列。 闲时重导消息到消息队列。 当主节点发生故障时,需要进行主备切换,可能会导致数据丢失。因一个数据库支持的最高并发访问数是有限的,可以将一个数据库的数据拆分到多个库中,来增加最高并发访问数。分表: 因一张表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL 语句的查询性能得到提升。
分库分表优势:分库分表后,承受的并发增加了多倍;磁盘使用率大大降低;单表数据量减少,SQL 执行效率明显提升。
水平拆分: 把一个表的数据拆分到多个数据库,每个数据库中的表结构不变。用多个库抗更高的并发。比如订单表每个月有500万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库。
垂直拆分: 把一个有很多字段的表,拆分成多张表到同一个库或多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如将一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)。
分库、分表的方式:
根据租户来分库、分表。 利用时间范围来分库、分表。 利用 ID 取模来分库、分表。 坑:
分库分表是一个运维层面需要做的事情,有时会采取凌晨宕机开始升级。可能熬夜到天亮,结果升级失败,则需要回滚,其实对技术团队都是一种煎熬。
分库分表看似光鲜亮丽,但分库分表会引入什么新的问题呢?唯一 ID 的生成方式有 n 种,各有各的用途,别用错了。唯一 ID。UUID 太长、占用空间大。 不具有有序性,作为主键时,在写入数据时,不能产生有顺序的 append 操作,只能进行 insert 操作,导致读取整个 B+
树节点到内存,插入记录后将整个节点写回磁盘,当记录占用空间很大的时候,性能很差。 缺点 获取系统当前时间作为唯一 ID。
高并发时,1 ms内可能有多个相同的 ID。 信息不安全 缺点 Twitter 的 snowflake
(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分
基本原理和优缺点:
算法。
基于 Snowflake 的优化算法。 借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 mysql 进行 ID 分配。 优点:解决了时间回拨和生成性能问题。
缺点:依赖 算法。来实现消息事务。 第一步:A 系统发送一个消息到 MQ,MQ将消息状态标记为 prepared
(预备状态,半消息),该消息无法被订阅。 第二步:MQ 响应 A 系统,告诉 A 系统已经接收到消息了。 第三步:A 系统执行本地事务。 第四步:若 A 系统执行本地事务成功,将 prepared
消息改为 commit
(提交事务消息),B 系统就可以订阅到消息了。 第五步:MQ 也会定时轮询所有 prepared
的消息,回调 A 系统,让 A 系统告诉 MQ 本地事务处理得怎么样了,是继续等待还是回滚。 第六步:A 系统检查本地事务的执行结果。 第七步:若 A 系统执行本地事务失败,则 MQ 收到 Rollback
信号,丢弃消息。若执行本地事务成功,则 MQ 收到 Commit
信号。 B 系统收到消息后,开始执行本地事务,如果执行失败,则自动不断重试直到成功。或 B 系统采取回滚的方式,同时要通过其他方式通知 A 系统也进行回滚。 B 系统需要保证幂等性。 分布式还有很多坑,这篇只是一个小小的总结,从这些坑中,我们也知道分布式有它的优势也有它的劣势,那到底该不该用分布式,完全取决于业务、时间、成本以及开发团队的综合实力。后续我会继续分享分布式中的一些底层原理,当然也少不了分享一些避坑指南。
参考资料:
美团的 Leaf-Snowflake 算法。
百度的 UIDGenerator 算法。
Advanced-Java
回复 悟空
领取优质资料。
❞「转发->在看->点赞->收藏->评论!!!」 是对我最大的支持!
精彩内容 TOP 5
和JavaGuide作者面基是种什么体验?
干货 | 45张图庖丁解牛18种Queue,你知道几种?
全网最细 | 21张图带你领略集合的线程不安全
程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?
5000字 | 24张图带你彻底理解Java中的21种锁
我是悟空,努力变强,变身超级赛亚人!
点亮
我被线程坑惨了!线程间的通信有毒!!!
在java中,线程间的通信可以使用wait
、notify
、notifyAll
来进行控制。从名字就可以看出来这3个方法都是跟多线程相关的,但是可能让你感到吃惊的是:这3个方法并不是Thread类或者是Runnable
接口的方法,而是Object类的3个本地方法。
其实要理解这一点也并不难,调用一个Object的wait与notify/notifyAll
的时候,必须保证调用代码对该Object是同步的,也就是说必须在作用等同于synchronized(obj){......}
的内部才能够去调用obj的wait与notify/notifyAll
三个方法,否则就会报错:
java.lang.IllegalMonitorStateException:current thread not owner
也就是说,在调用这3个方法的时候,当前线程必须获得这个对象的锁,那么这3个方法就是和对象锁相关的,所以是属于Object的方法而不是Thread,因为不是每个对象都是Thread。所以我们在理解wait、notify、notifyAll
之前,先要了解以下对象锁。
多个线程都持有同一个对象的时候,如果都要进入synchronized(obj){…}的内部,就必须拿到这个对象的锁,synchronized的机制保证了同一时间最多只能有1个线程拿到了对象的锁,如下图:
下面我们来看一下这3个方法的作用:
- wait:线程自动释放其占有的对象锁,并等待notify
- notify:唤醒一个正在wait当前对象锁的线程,并让它拿到对象锁
- notifyAll:唤醒所有正在wait前对象锁的线程
notify和notifyAll的最主要的区别是:notify只是唤醒一个正在wait当前对象锁的线程,而notifyAll唤醒所有。值得注意的是:notify是本地方法,具体唤醒哪一个线程由虚拟机控制;notifyAll后并不是所有的线程都能马上往下执行,它们只是跳出了wait状态,接下来它们还会是竞争对象锁。
下面通过一个常用生产者、消费者的例子来说明。
消息实体类:
package com.podongfeng;
/**
* Title: Message.class<br>
* Description: 消息实体<br>
* Create DateTime: 2016年04月17日 下午1:27 <br>
*
* @author podongfeng
*///加入Java开发交流君样:756584822一起吹水聊天
public class Message {
}
生产者:
package com.podongfeng;
import java.util.ArrayList;
import java.util.List;
/**
* Title: Producer.class<br>
* Description: 消息生产者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Producer extends Thread {
List<Message> msgList = new ArrayList<>();
@Override public void run() {
try {
while (true) {
Thread.sleep(3000);
Message msg = new Message();
synchronized(msgList) {
msgList.add(msg);
msgList.notify(); //这里只能是notify而不能是notifyAll,否则remove(0)会报java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
}
}
} catch (Exception e) {
e.printStackTrace();
}
}//加入Java开发交流君样:756584822一起吹水聊天
public Message waitMsg() {
synchronized(msgList) {
if(msgList.size() == 0) {
try {
msgList.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
return msgList.remove(0);
}
}//加入Java开发交流君样:756584822一起吹水聊天
}
消费者:
package com.podongfeng;
/**
* Title: Consumer.class<br>
* Description: 消息消费者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Consumer extends Thread {
private Producer producer;
public Consumer(String name, Producer producer) {
super(name);
this.producer = producer;
}
//加入Java开发交流君样:756584822一起吹水聊天
@Override public void run() {
while (true) {
Message msg = producer.waitMsg();
System.out.println("Consumer " + getName() + " get a msg");
}
}
public static void main(String[] args) {
Producer p = new Producer();
p.start();
new Consumer("Consumer1", p).start();
new Consumer("Consumer2", p).start();
new Consumer("Consumer3", p).start();
}
}//加入Java开发交流君样:756584822一起吹水聊天
消费者线程调用waitMsg去获取一个消息实体,如果msgList为空,则线程进入wait状态;生产这线程每隔3秒钟生产出体格msg实体并放入msgList列表,完成后,调用notify唤醒一个消费者线程去消费。
最后再次提醒注意:
wait、notify、notifyAll并不是Thread类或者是Runnable接口的方法,而是Object类的3个本地方法。
在调用这3个方法的时候,当前线程必须获得这个对象的锁
生命不止坚毅鱼奋斗,有梦想才是有意义的追求
给大家推荐一个免费的学习交流群:
最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。
以上是关于这三年被分布式坑惨了,曝光十大坑的主要内容,如果未能解决你的问题,请参考以下文章
偏移量
的概念,代表着消息的序号,每条消息写到消息队列都会有一个偏移量,消费者消费了数据之后,每过一段固定的时间,就会把消费过的消息的偏移量提交一下,表示已经消费过了,下次消费就从偏移量后面开始消费。坑:
当消费完消息后,还没来得及提交偏移量,系统就被关机了,那么未提交偏移量的消息则会再次被消费。
坑:
消息丢失会带来什么问题?如果是订单下单、支付结果通知、扣费相关的消息丢失,则可能造成财务损失,如果量很大,就会给甲方带来巨大损失。
confirm
模式来解决同步机制的性能问题。每次生产者发送的消息都会分配一个唯一的 id,如果写入到了 RabbitMQ 队列中,则 RabbitMQ 会回传一个 ack
消息,说明这个消息接收成功。如果 RabbitMQ 没能处理这个消息,则回调 nack
接口。说明需要重试发送消息。confirm
模式的区别:的时候将其设置为持久化。deliveryMode
设置为 2 。confirm
模式,可以重试发送消息。ack
,告诉消息队列我处理完了。问题: 那这种主动 ack
有什么漏洞了?如果 主动 ack
的时候挂了,怎么办?
则可能会被再次消费,这个时候就需要幂等处理了。
问题: 如果这条消息一直被重复消费怎么办?
则需要有加上重试次数的监测,如果超过一定次数则将消息丢失,记录到异常表或发送异常通知给值班人员。
的某个 broker(节点)宕机了,重新选举 leader (写入的节点)。如果 leader 挂了,follower 还有些数据未同步完,则 follower 成为 leader 后,消息队列会丢失一部分数据。解决方案
replication.factor
参数,值必须大于 1,要求每个 partition 必须有至少 2 个副本。min.insyc.replicas
必须大于 1,表示一个 leader 至少一个 follower 还跟自己保持联系。
坑:
用户先下单成功,然后取消订单,如果顺序颠倒,则最后数据库里面会有一条下单成功的订单。
RabbitMQ 场景:
RabbitMQ 解决方案:
Kafka 场景:
Kafka 解决方案:
消息积压:消息队列里面有很多消息来不及消费。
场景 1: 消费端出了问题,比如消费者都挂了,没有消费者来消费了,导致消息在队列里面不断积压。
场景 2: 消费端出了问题,比如消费者消费的速度太慢了,导致消息不断积压。
坑:比如线上正在做订单活动,下单全部走消息队列,如果消息不断积压,订单都没有下单成功,那么将会损失很多交易。
解决方案:解铃还须系铃人
坑:
RabbitMQ 可以设置过期时间,如果消息超过一定的时间还没有被消费,则会被 RabbitMQ 给清理掉。消息就丢失了。
解决方案:
坑:
当消息队列因消息积压导致的队列快写满,所以不能接收更多的消息了。生产者生产的消息将会被丢弃。
解决方案:
Purge Message
操作。当主节点发生故障时,需要进行主备切换,可能会导致数据丢失。因一个数据库支持的最高并发访问数是有限的,可以将一个数据库的数据拆分到多个库中,来增加最高并发访问数。分表: 因一张表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL 语句的查询性能得到提升。
分库分表优势:分库分表后,承受的并发增加了多倍;磁盘使用率大大降低;单表数据量减少,SQL 执行效率明显提升。
水平拆分: 把一个表的数据拆分到多个数据库,每个数据库中的表结构不变。用多个库抗更高的并发。比如订单表每个月有500万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库。
垂直拆分: 把一个有很多字段的表,拆分成多张表到同一个库或多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如将一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)。
分库、分表的方式:
根据租户来分库、分表。 利用时间范围来分库、分表。 利用 ID 取模来分库、分表。 坑:
分库分表是一个运维层面需要做的事情,有时会采取凌晨宕机开始升级。可能熬夜到天亮,结果升级失败,则需要回滚,其实对技术团队都是一种煎熬。
分库分表看似光鲜亮丽,但分库分表会引入什么新的问题呢?唯一 ID 的生成方式有 n 种,各有各的用途,别用错了。唯一 ID。UUID 太长、占用空间大。 不具有有序性,作为主键时,在写入数据时,不能产生有顺序的 append 操作,只能进行 insert 操作,导致读取整个 B+
树节点到内存,插入记录后将整个节点写回磁盘,当记录占用空间很大的时候,性能很差。 缺点 获取系统当前时间作为唯一 ID。
高并发时,1 ms内可能有多个相同的 ID。 信息不安全 缺点 Twitter 的 snowflake
(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分
基本原理和优缺点:
算法。
基于 Snowflake 的优化算法。 借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 mysql 进行 ID 分配。 优点:解决了时间回拨和生成性能问题。
缺点:依赖 算法。来实现消息事务。 第一步:A 系统发送一个消息到 MQ,MQ将消息状态标记为 prepared
(预备状态,半消息),该消息无法被订阅。 第二步:MQ 响应 A 系统,告诉 A 系统已经接收到消息了。 第三步:A 系统执行本地事务。 第四步:若 A 系统执行本地事务成功,将 prepared
消息改为 commit
(提交事务消息),B 系统就可以订阅到消息了。 第五步:MQ 也会定时轮询所有 prepared
的消息,回调 A 系统,让 A 系统告诉 MQ 本地事务处理得怎么样了,是继续等待还是回滚。 第六步:A 系统检查本地事务的执行结果。 第七步:若 A 系统执行本地事务失败,则 MQ 收到 Rollback
信号,丢弃消息。若执行本地事务成功,则 MQ 收到 Commit
信号。 B 系统收到消息后,开始执行本地事务,如果执行失败,则自动不断重试直到成功。或 B 系统采取回滚的方式,同时要通过其他方式通知 A 系统也进行回滚。 B 系统需要保证幂等性。 分布式还有很多坑,这篇只是一个小小的总结,从这些坑中,我们也知道分布式有它的优势也有它的劣势,那到底该不该用分布式,完全取决于业务、时间、成本以及开发团队的综合实力。后续我会继续分享分布式中的一些底层原理,当然也少不了分享一些避坑指南。
参考资料:
美团的 Leaf-Snowflake 算法。
百度的 UIDGenerator 算法。
Advanced-Java
回复 悟空
领取优质资料。
❞「转发->在看->点赞->收藏->评论!!!」 是对我最大的支持!
精彩内容 TOP 5
和JavaGuide作者面基是种什么体验?
干货 | 45张图庖丁解牛18种Queue,你知道几种?
全网最细 | 21张图带你领略集合的线程不安全
程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?
5000字 | 24张图带你彻底理解Java中的21种锁
我是悟空,努力变强,变身超级赛亚人!
点亮
我被线程坑惨了!线程间的通信有毒!!!
在java中,线程间的通信可以使用wait
、notify
、notifyAll
来进行控制。从名字就可以看出来这3个方法都是跟多线程相关的,但是可能让你感到吃惊的是:这3个方法并不是Thread类或者是Runnable
接口的方法,而是Object类的3个本地方法。
其实要理解这一点也并不难,调用一个Object的wait与notify/notifyAll
的时候,必须保证调用代码对该Object是同步的,也就是说必须在作用等同于synchronized(obj){......}
的内部才能够去调用obj的wait与notify/notifyAll
三个方法,否则就会报错:
java.lang.IllegalMonitorStateException:current thread not owner
也就是说,在调用这3个方法的时候,当前线程必须获得这个对象的锁,那么这3个方法就是和对象锁相关的,所以是属于Object的方法而不是Thread,因为不是每个对象都是Thread。所以我们在理解wait、notify、notifyAll
之前,先要了解以下对象锁。
多个线程都持有同一个对象的时候,如果都要进入synchronized(obj){…}的内部,就必须拿到这个对象的锁,synchronized的机制保证了同一时间最多只能有1个线程拿到了对象的锁,如下图:
下面我们来看一下这3个方法的作用:
- wait:线程自动释放其占有的对象锁,并等待notify
- notify:唤醒一个正在wait当前对象锁的线程,并让它拿到对象锁
- notifyAll:唤醒所有正在wait前对象锁的线程
notify和notifyAll的最主要的区别是:notify只是唤醒一个正在wait当前对象锁的线程,而notifyAll唤醒所有。值得注意的是:notify是本地方法,具体唤醒哪一个线程由虚拟机控制;notifyAll后并不是所有的线程都能马上往下执行,它们只是跳出了wait状态,接下来它们还会是竞争对象锁。
下面通过一个常用生产者、消费者的例子来说明。
消息实体类:
package com.podongfeng;
/**
* Title: Message.class<br>
* Description: 消息实体<br>
* Create DateTime: 2016年04月17日 下午1:27 <br>
*
* @author podongfeng
*///加入Java开发交流君样:756584822一起吹水聊天
public class Message {
}
生产者:
package com.podongfeng;
import java.util.ArrayList;
import java.util.List;
/**
* Title: Producer.class<br>
* Description: 消息生产者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Producer extends Thread {
List<Message> msgList = new ArrayList<>();
@Override public void run() {
try {
while (true) {
Thread.sleep(3000);
Message msg = new Message();
synchronized(msgList) {
msgList.add(msg);
msgList.notify(); //这里只能是notify而不能是notifyAll,否则remove(0)会报java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
}
}
} catch (Exception e) {
e.printStackTrace();
}
}//加入Java开发交流君样:756584822一起吹水聊天
public Message waitMsg() {
synchronized(msgList) {
if(msgList.size() == 0) {
try {
msgList.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
return msgList.remove(0);
}
}//加入Java开发交流君样:756584822一起吹水聊天
}
消费者:
package com.podongfeng;
/**
* Title: Consumer.class<br>
* Description: 消息消费者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Consumer extends Thread {
private Producer producer;
public Consumer(String name, Producer producer) {
super(name);
this.producer = producer;
}
//加入Java开发交流君样:756584822一起吹水聊天
@Override public void run() {
while (true) {
Message msg = producer.waitMsg();
System.out.println("Consumer " + getName() + " get a msg");
}
}
public static void main(String[] args) {
Producer p = new Producer();
p.start();
new Consumer("Consumer1", p).start();
new Consumer("Consumer2", p).start();
new Consumer("Consumer3", p).start();
}
}//加入Java开发交流君样:756584822一起吹水聊天
消费者线程调用waitMsg去获取一个消息实体,如果msgList为空,则线程进入wait状态;生产这线程每隔3秒钟生产出体格msg实体并放入msgList列表,完成后,调用notify唤醒一个消费者线程去消费。
最后再次提醒注意:
wait、notify、notifyAll并不是Thread类或者是Runnable接口的方法,而是Object类的3个本地方法。
在调用这3个方法的时候,当前线程必须获得这个对象的锁
生命不止坚毅鱼奋斗,有梦想才是有意义的追求
给大家推荐一个免费的学习交流群:
最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。
以上是关于这三年被分布式坑惨了,曝光十大坑的主要内容,如果未能解决你的问题,请参考以下文章
分表: 因一张表的数据量太大,用索引来查询数据都搞不定了,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL 语句的查询性能得到提升。
分库分表优势:分库分表后,承受的并发增加了多倍;磁盘使用率大大降低;单表数据量减少,SQL 执行效率明显提升。
水平拆分: 把一个表的数据拆分到多个数据库,每个数据库中的表结构不变。用多个库抗更高的并发。比如订单表每个月有500万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库。
垂直拆分: 把一个有很多字段的表,拆分成多张表到同一个库或多个库上面。高频访问字段放到一张表,低频访问的字段放到另外一张表。利用数据库缓存来缓存高频访问的行数据。比如将一张很多字段的订单表拆分成几张表分别存不同的字段(可以有冗余字段)。
分库、分表的方式:
坑:
分库分表是一个运维层面需要做的事情,有时会采取凌晨宕机开始升级。可能熬夜到天亮,结果升级失败,则需要回滚,其实对技术团队都是一种煎熬。
分库分表看似光鲜亮丽,但分库分表会引入什么新的问题呢?唯一 ID 的生成方式有 n 种,各有各的用途,别用错了。唯一 ID。UUID 太长、占用空间大。 不具有有序性,作为主键时,在写入数据时,不能产生有顺序的 append 操作,只能进行 insert 操作,导致读取整个 B+
树节点到内存,插入记录后将整个节点写回磁盘,当记录占用空间很大的时候,性能很差。 缺点 获取系统当前时间作为唯一 ID。
高并发时,1 ms内可能有多个相同的 ID。 信息不安全 缺点 Twitter 的 snowflake
(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分
基本原理和优缺点:
算法。
基于 Snowflake 的优化算法。 借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 mysql 进行 ID 分配。 优点:解决了时间回拨和生成性能问题。
缺点:依赖 算法。来实现消息事务。 第一步:A 系统发送一个消息到 MQ,MQ将消息状态标记为 prepared
(预备状态,半消息),该消息无法被订阅。 第二步:MQ 响应 A 系统,告诉 A 系统已经接收到消息了。 第三步:A 系统执行本地事务。 第四步:若 A 系统执行本地事务成功,将 prepared
消息改为 commit
(提交事务消息),B 系统就可以订阅到消息了。 第五步:MQ 也会定时轮询所有 prepared
的消息,回调 A 系统,让 A 系统告诉 MQ 本地事务处理得怎么样了,是继续等待还是回滚。 第六步:A 系统检查本地事务的执行结果。 第七步:若 A 系统执行本地事务失败,则 MQ 收到 Rollback
信号,丢弃消息。若执行本地事务成功,则 MQ 收到 Commit
信号。 B 系统收到消息后,开始执行本地事务,如果执行失败,则自动不断重试直到成功。或 B 系统采取回滚的方式,同时要通过其他方式通知 A 系统也进行回滚。 B 系统需要保证幂等性。 分布式还有很多坑,这篇只是一个小小的总结,从这些坑中,我们也知道分布式有它的优势也有它的劣势,那到底该不该用分布式,完全取决于业务、时间、成本以及开发团队的综合实力。后续我会继续分享分布式中的一些底层原理,当然也少不了分享一些避坑指南。
参考资料:
美团的 Leaf-Snowflake 算法。
百度的 UIDGenerator 算法。
Advanced-Java
回复 悟空
领取优质资料。
❞「转发->在看->点赞->收藏->评论!!!」 是对我最大的支持!
精彩内容 TOP 5
和JavaGuide作者面基是种什么体验?
干货 | 45张图庖丁解牛18种Queue,你知道几种?
全网最细 | 21张图带你领略集合的线程不安全
程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?
5000字 | 24张图带你彻底理解Java中的21种锁
我是悟空,努力变强,变身超级赛亚人!
点亮
我被线程坑惨了!线程间的通信有毒!!!
在java中,线程间的通信可以使用wait
、notify
、notifyAll
来进行控制。从名字就可以看出来这3个方法都是跟多线程相关的,但是可能让你感到吃惊的是:这3个方法并不是Thread类或者是Runnable
接口的方法,而是Object类的3个本地方法。
其实要理解这一点也并不难,调用一个Object的wait与notify/notifyAll
的时候,必须保证调用代码对该Object是同步的,也就是说必须在作用等同于synchronized(obj){......}
的内部才能够去调用obj的wait与notify/notifyAll
三个方法,否则就会报错:
java.lang.IllegalMonitorStateException:current thread not owner
也就是说,在调用这3个方法的时候,当前线程必须获得这个对象的锁,那么这3个方法就是和对象锁相关的,所以是属于Object的方法而不是Thread,因为不是每个对象都是Thread。所以我们在理解wait、notify、notifyAll
之前,先要了解以下对象锁。
多个线程都持有同一个对象的时候,如果都要进入synchronized(obj){…}的内部,就必须拿到这个对象的锁,synchronized的机制保证了同一时间最多只能有1个线程拿到了对象的锁,如下图:
下面我们来看一下这3个方法的作用:
- wait:线程自动释放其占有的对象锁,并等待notify
- notify:唤醒一个正在wait当前对象锁的线程,并让它拿到对象锁
- notifyAll:唤醒所有正在wait前对象锁的线程
notify和notifyAll的最主要的区别是:notify只是唤醒一个正在wait当前对象锁的线程,而notifyAll唤醒所有。值得注意的是:notify是本地方法,具体唤醒哪一个线程由虚拟机控制;notifyAll后并不是所有的线程都能马上往下执行,它们只是跳出了wait状态,接下来它们还会是竞争对象锁。
下面通过一个常用生产者、消费者的例子来说明。
消息实体类:
package com.podongfeng;
/**
* Title: Message.class<br>
* Description: 消息实体<br>
* Create DateTime: 2016年04月17日 下午1:27 <br>
*
* @author podongfeng
*///加入Java开发交流君样:756584822一起吹水聊天
public class Message {
}
生产者:
package com.podongfeng;
import java.util.ArrayList;
import java.util.List;
/**
* Title: Producer.class<br>
* Description: 消息生产者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Producer extends Thread {
List<Message> msgList = new ArrayList<>();
@Override public void run() {
try {
while (true) {
Thread.sleep(3000);
Message msg = new Message();
synchronized(msgList) {
msgList.add(msg);
msgList.notify(); //这里只能是notify而不能是notifyAll,否则remove(0)会报java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
}
}
} catch (Exception e) {
e.printStackTrace();
}
}//加入Java开发交流君样:756584822一起吹水聊天
public Message waitMsg() {
synchronized(msgList) {
if(msgList.size() == 0) {
try {
msgList.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
return msgList.remove(0);
}
}//加入Java开发交流君样:756584822一起吹水聊天
}
消费者:
package com.podongfeng;
/**
* Title: Consumer.class<br>
* Description: 消息消费者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Consumer extends Thread {
private Producer producer;
public Consumer(String name, Producer producer) {
super(name);
this.producer = producer;
}
//加入Java开发交流君样:756584822一起吹水聊天
@Override public void run() {
while (true) {
Message msg = producer.waitMsg();
System.out.println("Consumer " + getName() + " get a msg");
}
}
public static void main(String[] args) {
Producer p = new Producer();
p.start();
new Consumer("Consumer1", p).start();
new Consumer("Consumer2", p).start();
new Consumer("Consumer3", p).start();
}
}//加入Java开发交流君样:756584822一起吹水聊天
消费者线程调用waitMsg去获取一个消息实体,如果msgList为空,则线程进入wait状态;生产这线程每隔3秒钟生产出体格msg实体并放入msgList列表,完成后,调用notify唤醒一个消费者线程去消费。
最后再次提醒注意:
wait、notify、notifyAll并不是Thread类或者是Runnable接口的方法,而是Object类的3个本地方法。
在调用这3个方法的时候,当前线程必须获得这个对象的锁
生命不止坚毅鱼奋斗,有梦想才是有意义的追求
给大家推荐一个免费的学习交流群:
最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。
以上是关于这三年被分布式坑惨了,曝光十大坑的主要内容,如果未能解决你的问题,请参考以下文章
B+
树节点到内存,插入记录后将整个节点写回磁盘,当记录占用空间很大的时候,性能很差。获取系统当前时间作为唯一 ID。
Twitter 的 snowflake
(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分
基本原理和优缺点:
算法。
prepared
(预备状态,半消息),该消息无法被订阅。prepared
消息改为 commit
(提交事务消息),B 系统就可以订阅到消息了。prepared
的消息,回调 A 系统,让 A 系统告诉 MQ 本地事务处理得怎么样了,是继续等待还是回滚。Rollback
信号,丢弃消息。若执行本地事务成功,则 MQ 收到 Commit
信号。分布式还有很多坑,这篇只是一个小小的总结,从这些坑中,我们也知道分布式有它的优势也有它的劣势,那到底该不该用分布式,完全取决于业务、时间、成本以及开发团队的综合实力。后续我会继续分享分布式中的一些底层原理,当然也少不了分享一些避坑指南。
参考资料:
美团的 Leaf-Snowflake 算法。
百度的 UIDGenerator 算法。
Advanced-Java
回复 悟空
领取优质资料。
「转发->在看->点赞->收藏->评论!!!」 是对我最大的支持!
精彩内容 TOP 5
和JavaGuide作者面基是种什么体验?
干货 | 45张图庖丁解牛18种Queue,你知道几种?
全网最细 | 21张图带你领略集合的线程不安全
程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?
5000字 | 24张图带你彻底理解Java中的21种锁
点亮
我被线程坑惨了!线程间的通信有毒!!!
在java中,线程间的通信可以使用wait
、notify
、notifyAll
来进行控制。从名字就可以看出来这3个方法都是跟多线程相关的,但是可能让你感到吃惊的是:这3个方法并不是Thread类或者是Runnable
接口的方法,而是Object类的3个本地方法。
其实要理解这一点也并不难,调用一个Object的wait与notify/notifyAll
的时候,必须保证调用代码对该Object是同步的,也就是说必须在作用等同于synchronized(obj){......}
的内部才能够去调用obj的wait与notify/notifyAll
三个方法,否则就会报错:
java.lang.IllegalMonitorStateException:current thread not owner
也就是说,在调用这3个方法的时候,当前线程必须获得这个对象的锁,那么这3个方法就是和对象锁相关的,所以是属于Object的方法而不是Thread,因为不是每个对象都是Thread。所以我们在理解wait、notify、notifyAll
之前,先要了解以下对象锁。
多个线程都持有同一个对象的时候,如果都要进入synchronized(obj){…}的内部,就必须拿到这个对象的锁,synchronized的机制保证了同一时间最多只能有1个线程拿到了对象的锁,如下图:
下面我们来看一下这3个方法的作用:
- wait:线程自动释放其占有的对象锁,并等待notify
- notify:唤醒一个正在wait当前对象锁的线程,并让它拿到对象锁
- notifyAll:唤醒所有正在wait前对象锁的线程
notify和notifyAll的最主要的区别是:notify只是唤醒一个正在wait当前对象锁的线程,而notifyAll唤醒所有。值得注意的是:notify是本地方法,具体唤醒哪一个线程由虚拟机控制;notifyAll后并不是所有的线程都能马上往下执行,它们只是跳出了wait状态,接下来它们还会是竞争对象锁。
下面通过一个常用生产者、消费者的例子来说明。
消息实体类:
package com.podongfeng;
/**
* Title: Message.class<br>
* Description: 消息实体<br>
* Create DateTime: 2016年04月17日 下午1:27 <br>
*
* @author podongfeng
*///加入Java开发交流君样:756584822一起吹水聊天
public class Message {
}
生产者:
package com.podongfeng;
import java.util.ArrayList;
import java.util.List;
/**
* Title: Producer.class<br>
* Description: 消息生产者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Producer extends Thread {
List<Message> msgList = new ArrayList<>();
@Override public void run() {
try {
while (true) {
Thread.sleep(3000);
Message msg = new Message();
synchronized(msgList) {
msgList.add(msg);
msgList.notify(); //这里只能是notify而不能是notifyAll,否则remove(0)会报java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
}
}
} catch (Exception e) {
e.printStackTrace();
}
}//加入Java开发交流君样:756584822一起吹水聊天
public Message waitMsg() {
synchronized(msgList) {
if(msgList.size() == 0) {
try {
msgList.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
return msgList.remove(0);
}
}//加入Java开发交流君样:756584822一起吹水聊天
}
消费者:
package com.podongfeng;
/**
* Title: Consumer.class<br>
* Description: 消息消费者<br>
* Create DateTime: 2016年04月17日 下午1:28 <br>
*
* @author podongfeng
*/
public class Consumer extends Thread {
private Producer producer;
public Consumer(String name, Producer producer) {
super(name);
this.producer = producer;
}
//加入Java开发交流君样:756584822一起吹水聊天
@Override public void run() {
while (true) {
Message msg = producer.waitMsg();
System.out.println("Consumer " + getName() + " get a msg");
}
}
public static void main(String[] args) {
Producer p = new Producer();
p.start();
new Consumer("Consumer1", p).start();
new Consumer("Consumer2", p).start();
new Consumer("Consumer3", p).start();
}
}//加入Java开发交流君样:756584822一起吹水聊天
消费者线程调用waitMsg去获取一个消息实体,如果msgList为空,则线程进入wait状态;生产这线程每隔3秒钟生产出体格msg实体并放入msgList列表,完成后,调用notify唤醒一个消费者线程去消费。
最后再次提醒注意:
wait、notify、notifyAll并不是Thread类或者是Runnable接口的方法,而是Object类的3个本地方法。
在调用这3个方法的时候,当前线程必须获得这个对象的锁
生命不止坚毅鱼奋斗,有梦想才是有意义的追求
给大家推荐一个免费的学习交流群:
最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。
以上是关于这三年被分布式坑惨了,曝光十大坑的主要内容,如果未能解决你的问题,请参考以下文章