-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 120 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 120 KB
1
{"meta":{"title":"YUYUYU's Blog","subtitle":null,"description":null,"author":"YUYUYU","url":"https://firecarrrr.github.io","root":"/"},"pages":[{"title":"categories","date":"2019-06-15T11:05:12.000Z","updated":"2019-06-15T11:05:12.073Z","comments":true,"path":"categories/index.html","permalink":"https://firecarrrr.github.io/categories/index.html","excerpt":"","text":""},{"title":"tags","date":"2019-06-15T11:04:49.000Z","updated":"2019-06-15T11:04:49.928Z","comments":true,"path":"tags/index.html","permalink":"https://firecarrrr.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"Dubbo 集群容错与负载均衡","slug":"Dubbo-集群容错与负载均衡","date":"2020-07-13T08:54:41.000Z","updated":"2020-07-14T07:07:48.712Z","comments":true,"path":"2020/07/13/Dubbo-集群容错与负载均衡/","link":"","permalink":"https://firecarrrr.github.io/2020/07/13/Dubbo-集群容错与负载均衡/","excerpt":"","text":"Dubbo 容错策略在设计系统时,不光要考虑正常情况下,代码逻辑怎么走,还要考虑异常情况下,代码逻辑怎么走。当服务消费方调用服务提供方的服务出现错误时,Dubbo 提供了多种容错方案。 Dubbo 容错方案 Failover 是 Dubbo 的默认容错模式,可以配置重试次数,通常用于读操作或者幂等的写操作。重试会导致接口延时增大,当下游服务器负载达到极限时,重试会加重下游服务器的负担。 Failfast 通常用在非幂等的接口调用上 Failsafe 通常用在佛系调用场景,即不关心调用是否成功,并且不想抛异常影响外层调用。 Failback 请求失败后,会自动记录在失败队列中,并由一个定时线程池定时重试,适用于一些异步或者最终一致性请求。 Forking 适用于对实时性要求较高的调用,但会浪费更多的服务资源。可以通过 forks 设置最大并行数。 Broadcast 不需要做负载均衡,通常用于服务状态更新后的广播。 实现 img Dubbo 负载均衡策略当服务提供方是集群时,为了避免大量请求一直集中在一个或者几个服务提供方机器上,从而导致这些机器的负载很高,甚至导致服务不可用,需要做一定的负载均衡策略 Dubbo 负载均衡 Random 默认负载均衡策略 RoundRobin 轮询策略。会存在执行很慢的服务提供者堆积请求的问题,当很多新请求到达该机器后,由于之前的请求还没有处理完,会导致新的请求被堆积。新版本中使用了类似 Ngnix 的平滑轮询算法,改善了请求堆积的问题。 LeastActive 最少活跃调用。在每个服务提供者里维护着一个活跃计数器,用来记录当前同时处理请求的个数,也就是并发处理任务的个数。这个值越小,说明当前服务提供者处理速度越快或者当前机器的负载越低。所以路由选择时,就选择这个活跃度最低的机器。如果活跃度相同,则随机选择一个。 ConsistentHash 一致性 Hash,可以保证相同参数的请求总是发往同一个提供者。当某一台提供者宕机时,原本发往改提供者的请求,将基于虚拟节点平摊给其它提供者,这样不会引起剧烈变动。 RoundRobin 负载均衡的实现RoundRobin 实现了平滑的轮询算法,这个算法的逻辑是: 每次做负载均衡时,遍历所有可选节点(Invoker 列表)。对于每个 Invoker,让他的 current = current + weight。 123// 考虑到并发场景下,某个 invoker 会被同时选中,current 表示节点被线程选中的权重总和// 例:某个节点权重是 100,被 4 个线程同时选中,则变成 400private AtomicLong current = new AtomicLong(0); 同时累加所有 Invoker 的 weight 到 totalWeight。 遍历完所有 Invoker 后,current 值最大的节点就是本次要选择的节点。把该节点的 current 值减去 totalWeight ,current = current - totalWeight。 回到 1 重复。 ConsistentHash 负载均衡的实现普通一致性 Hash 当节点较少时,可能由于散列不是很均匀,容易造成某些节点压力大(一致性 Hash 倾斜问题)。Dubbo 框架使用了优化过的 Ketama 一致性 Hash。这种算法会给每个真实节点创建多个虚拟节点,让节点环形上的分布更加均匀,后续调用也会随之更加均匀。 img 上图中,相同颜色的节点,属于同一服务提供者。这样做的目的是通过引入虚拟节点,让 Invoker 在圆环上分散开来,避免数据倾斜的问题。","categories":[{"name":"Distributed System","slug":"Distributed-System","permalink":"https://firecarrrr.github.io/categories/Distributed-System/"}],"tags":[{"name":"Dubbo","slug":"Dubbo","permalink":"https://firecarrrr.github.io/tags/Dubbo/"}]},{"title":"Spring 中的设计模式","slug":"Spring-中的设计模式","date":"2020-07-06T09:26:44.000Z","updated":"2020-07-06T09:26:44.558Z","comments":true,"path":"2020/07/06/Spring-中的设计模式/","link":"","permalink":"https://firecarrrr.github.io/2020/07/06/Spring-中的设计模式/","excerpt":"","text":"工厂模式简单工厂模式简单工厂模式就是把对象的创建工作交个一个工厂类来做。例如: 1234567891011public class TestCaseFactory { public static ITestCase createTestCase(String caseType) { ITestCase case = null; if(..){ case = ... } else if(..) { case = ... } return case; }} 如果这个对象是可复用的,可以做一下缓存。 Spring 的 BeanFactory 就是简单工厂模式的体现: 12BeanFactory bf = new ClassPathXmlApplicationContext(\"spring.xml\");User userBean = (User) bf.getBean(\"userBean\"); 工厂方法模式在简单工厂模式中,所有的逻辑判断、实例创建都是在工厂类中进行的。 如果实例的创建逻辑非常复杂,可以为不同的产品提供不同的工厂,不同的工厂生产不同的产品,每一个工厂都只对应一个相应的对象。 Spring 的 FactoryBean 就是这种思想的提现,FactoryBean 帮助实现复杂的 Bean 的实例化和初始化逻辑。调用容器的 getBean() 方法,返回的是 FactoryBean 的 getObject() 的结果。 12345public interface FactoryBean<T> { T getObject(); Class<?> getObjectType(); boolean isSingleton();} 代理模式AOP 基于代理模式 装饰器模式装饰器模式利用组合的方式来对基本类的功能进行增强,装饰器类和基本类都实现了同一个接口,基础类以组合的方式被加载到装饰器中,装饰器来对代码功能进行增强。 12345678910111213141516171819202122public interface IA { void f();}public class A implements IA { public void f() { // ....}}// 装饰器类public class ADecorator implements IA { private IA a; public ADecorator(IA a) { this.a = a; } public void f() { // 功能增强 a.f(); } public void d() { // 利用 A,实现 A 没有的功能 }} 装饰器模式在 Java IO 中的应用: 123456InputStream in = new FileInputStream(\"/user/wangzheng/test.txt\");InputStream bin = new BufferedInputStream(in);byte[] data = new byte[128];while (bin.read(data) != -1) { //...} 适配器模式适配器模式是用来做适配的,将原本不兼容的接口转换为可兼容的接口。 适配器模式有两种: 1:类适配器 1234567891011121314151617181920212223242526// 要转化成的目标接口定义public interface ITarget { void f1(); void f2(); void fc();}// 不兼容 ITarget 接口定义的类public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } }// 将 Adaptee 转化为兼容 ITarget 接口的类public class Adaptor extends Adaptee implements ITarget { public void f1() { super.fa(); } public void f2() { // ...重新实现 f2() ... } // fc() 不需要实现,直接继承自 Adaptee} 类适配器是基于继承实现的。 2:对象适配器 123456789101112131415161718192021222324252627282930313233// 要转化成的目标接口定义public interface ITarget { void f1(); void f2(); void fc();}// 不兼容 ITarget 接口定义的类public class Adaptee { public void fa() { //... } public void fb() { //... } public void fc() { //... } } public class Adaptor implements ITarget { private Adaptee adaptee; public Adaptor(Adaptee adaptee) { this.adaptee = adaptee; } public void f1() { adaptee.fa(); } public void f2() { // 重新实现 f2() ... } public void fc() { adaptee.fc(); }} 对象适配器是基于组合实现的。 模板模式模板模式就是在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板模式可以让子类在不改变算法整体框架的情况下,重新定义算法中某些步骤。 一个例子: 1234567891011public abstract class AbstractClass { public final void templateMethod() { //... method1(); //... method2(); } protected abstract void method1(); protected abstract void method2();} 上面的类中,templateMethod 方法定义了算法执行的流程,method1 和 method2 的实现推迟到了子类中。 HttpServlet 类是模板方法的一个例子,子类实现其中的 doGet() 和 doPost() 方法,HttpServlet 中的 service() 方法定义了流程的骨架。 职责链模式","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[{"name":"Spring","slug":"Spring","permalink":"https://firecarrrr.github.io/tags/Spring/"}]},{"title":"分布式锁","slug":"分布式锁","date":"2020-07-06T09:26:03.000Z","updated":"2020-07-06T09:27:44.482Z","comments":true,"path":"2020/07/06/分布式锁/","link":"","permalink":"https://firecarrrr.github.io/2020/07/06/分布式锁/","excerpt":"","text":"什么是分布式锁?在分布式环境下,对共享资源的访问或者一些同步操作时需要分布式锁来协助的。 比如说假设一个场景,一个订单服务,允许一个用户一个商品只能下一单。订单服务有多个实例,两个并发的请求被负载均衡分配到了不同的实例上,这个两个实例同时创建订单,那么就会出现一个商品下了多单的情况。 这种情况,就需要在下单时加上分布式锁,防止其它实例同时下单。 分布式锁服务应该具备的特点 安全性:在任意时刻,只有一个客户端可以获得锁 避免死锁:客户端最终一定可以获得锁,即使当前持有锁的客户端在释放锁之前崩溃或者网络不可达 容错性:只要锁服务集群中的大部分节点存活,Client 就可以执行加锁操作 分布式锁服务的实现分布式锁服务,一般可以用 DB,Redis,ZooKeeper 等实现 Redis 分布式锁服务单机方案1SET resource_name my_random_value NX PX 30000 这条命令在不存在这个 key 的情况下(NX 选项),会设置一个随机的 value 赋给这个 key,并设置一个超时时间 30000 ms 然后通过下面的算法来释放这个锁: 12345if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1])else return 0end 只有当 key 对应的 value 和客户端设置的随机值相等时,才去删除这个 key。这样可以防止一个客户端释放了另一个客户端申请的锁。(比如,Client A 获取了锁,然后阻塞在某个耗时操作上了,锁自动释放了。然后,B 获取了锁。A 此时醒过来要去释放锁,如果没有上述保证,是会出问题的)。 另外,这个方案还有其它问题: img 假设 A 获取了锁 A 挂在了某些耗时操作上,比如一个外部的阻塞调用,或是 CPU 被别的进程吃满,或者碰上了 full gc,导致 A 花了超过平时几倍的时间 A 获取的锁超时,自动释放 B 获取锁并更新了资源 A 醒过来,也去更新了资源。于是,就是 B 的更新给冲掉了 RedLock 算法假设有 N 个 redis 节点,这 N 个节点是独立的,没有任何主备关系和额外的协调系统。 当申请一个 key 时,客户端做以下操作: 获取当前时间戳 用同样的 key 和随机值,顺序的尝试从这 N 个节点上获取锁。获取锁应该设置一个超时时间,这个超时时间应该比锁自动释放的时间短很多,以防止某些节点挂掉的情况。 计算获取锁花费的时间(当前时间减去第一步的时间戳),只有当获取锁话费的时间比锁的有效时间短,并且在大多数节点都获取到锁的情况才能判断客户端获取到了锁。 锁的有效时间变成了超时时间减去获取锁花费的时间 如果客户端获取锁失败了,它会尝试在所以节点上解锁 失败重试当客户端尝试获取锁失败的时候,应该隔一个小的 random delay 过后去重试。因为这个是 random delay,所以在一定程度上可以避免多个客户端同时尝试去获取锁的时候造成的 split brain 问题。(多个客户端同时去获取锁,假设一共 5 个节点,一个获取到 2 个,一个获取到 2 个,一个获取到 1 个,最后谁都没有获取到锁)。 客户端越快的获取多数节点上的锁,split brain 发生的时间窗口就越小,所以理想情况下客户端应该利用多线程技术,同时将 SET 请求发送到 N 个实例上。 获取锁失败的客户端立即释放所有节点上的锁是非常重要的,不然其它线程就必须等到这个 key 超时之后才能获取这个节点上的锁了。 当某个客户端出现网络分区的时候,那就只能等那个客户端设置的锁自动超时了。 DB 实现分布式锁基于唯一索引的实现数据库里面加一张表,设置关于资源的唯一索引。 获取锁,就是插入一条数据,如果已经有相应资源的记录,那就会抛出异常。 锁释放就是删除对应资源。 问题: 加锁线程挂了,没办法解锁 非可重入锁,同一个线程没有解锁,没办法重入 Zookeeper 实现分布式锁 image-20200630224052012 创建的节点必须是临时节点 对每个资源,节点的名字必须是唯一的","categories":[{"name":"Distributed System","slug":"Distributed-System","permalink":"https://firecarrrr.github.io/categories/Distributed-System/"}],"tags":[]},{"title":"redis sentinel","slug":"redis-sentinel","date":"2020-07-05T09:10:36.000Z","updated":"2020-07-05T09:11:39.691Z","comments":true,"path":"2020/07/05/redis-sentinel/","link":"","permalink":"https://firecarrrr.github.io/2020/07/05/redis-sentinel/","excerpt":"","text":"什么是 Sentinel?Sentinel(哨兵)是 redis 高可用的一个解决方案。sentinel 本质上是运行在特殊状态下的 redis 服务器。 由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器的所有从服务器。在被监视主服务器进入下线状态时,自动将下线的主服务器属下的某个从服务器升级为主服务器,然后由新的主服务器代替已下线的主服务器处理命令请求。 image-20200705143153699 故障转移的过程 当 master 服务器客观下线后,leader sentinel 开始对 master 服务器进行故障转移操作。 sentinel 从所有从服务其中选举一个从服务器,被选中的从服务器被升级为新的主服务器。 sentinel 向原 master 服务器所有的从服务器发送新的复制命令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移完毕。 sentinel 还会建设已下线的 master 服务器,并在它重新上线时,将它设置为新的主服务器的从服务器。 故障转移日志看一下 sentinel 的日志 image-20200705155037719 首先,sentinel 注意到 master 节点挂了 sentinel 集群中的节点需要就主节点挂了这件事情达成共识,因为现在只有一个节点,所以是配的 quorum 1/1 达到故障转移的条件 在从节点中进行主节点的选举 选举除了新的主节点 在选举出的节点上执行 slave of none 命令,使之成为主节点 转化到新的主节点上 Sentinel 基本实现原理sentinel 会与被监视的主服务器和其从服务器建立两个连接: 命令连接 订阅连接 获取主服务器信息sentinel 默认每 10s 向监视的主服务器发送 info 命令。并通过分析 info 命令的回复来获取主服务器当前的信息。 配置 sentinel 的时候并不需要配置从服务器信息,因为这是通过 info 命令自动发现的。 获取从服务器信息当发现有新的从服务器出现时,Sentinel 会创建连接到从服务器的命令连接和订阅连接。 通过命令连接,sentinel 也会每 10s 向从服务器发送 info 命令。 向主服务器和从服务器发送信息默认情况下,Sentinel 会以每 2s 一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送一条发布命令: 1PUBLISH _sentinel_:hello \"<s.ip>,<s.port>,<s.runid>,<s.epoch>,<m.name>,<m.ip>,<m.port>,<m.epoch>\" s 开头的是 sentinel 的信息 m 开头的是 master 的信息 接收来自主服务器和从服务器的频道信息当 sentinel 与一个主服务器和从服务器建立订阅连接之后,sentinel 就会通过订阅连接发送一下命令: 1SUBSCRIBE _sentinel_:hello sentinel 对这个频道的订阅会持续到 sentinel 与服务器连接断开为止。 也就是说每个与 sentinel 连接的服务器,sentinel 既通过命令连接向服务器发送消息,又通过订阅连接从服务器接收消息 sentinel:hello 频道的消息 对于监视一个服务器的多个 sentinel 来说,一个 sentinel 发送的消息会被其他 sentinel 收到,这些消息被用于更新其他 sentinel 对发送消息 sentinel 的认识,也用于更新其他 sentinel 对被监视服务器的认识。 image-20200705164638275 创建连向其他 Sentinel 的命令连接当 sentinel 通过订阅频道发现一个新的 Sentinel 时,会创建一个连向新 sentinel 的命令连接。新 sentinel 也会创建连向这个 sentinel 的命令连接,最终监视同一主服务器的多个 sentinel 将形成相互连接的网络。 image-20200705164508764 检测主观下线默认情况下,Sentinel 会以每秒一次的频率向所有它创建了命令连接的实例(包括主服务器、从服务器、其它 sentinel)发送 ping 命令,以此判断实例是否在线。 123456sentinel down-after-milliseconds <master-name> <milliseconds>## Number of milliseconds the master (or any attached replica or sentinel) should# be unreachable (as in, not acceptable reply to PING, continuously, for the# specified period) in order to consider it in S_DOWN state (Subjectively# Down). 通过这个设置项设置判断节点下线的 timeout ,如果超过这个时间范围都没有收到有效回复就会判断节点主观下线。 检查客观下线当 sentinel 将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,会向同样监视这一主服务器的其他 sentinel 进行询问。当 sentinel 从其他 sentinel 那里接收到足够数量的已下线判断后,sentinel 将会把这个服务器判定为客观下线,并开始进行故障转移。 1234567891011121314151617sentinel monitor <master-name> <ip> <redis-port> <quorum>## Tells Sentinel to monitor this master, and to consider it in O_DOWN# (Objectively Down) state only if at least <quorum> sentinels agree.## Note that whatever is the ODOWN quorum, a Sentinel will require to# be elected by the majority of the known Sentinels in order to# start a failover, so no failover can be performed in minority.## Replicas are auto-discovered, so you don't need to specify replicas in# any way. Sentinel itself will rewrite this configuration file adding# the replicas using additional configuration options.# Also note that the configuration file is rewritten when a# replica is promoted to master.## Note: master name should not include special characters or spaces.# The valid charset is A-z 0-9 and the three characters \".-_\". 这个设置用于设置客观下线需要达成共识最低的票数。 选举 leader Sentinel当一个主服务器被判定为客观下线时,监视这个下线主服务器的各个 sentinel 会进行协调,选出一个 leader sentinel,并由 leader sentinel 对下线主服务器执行故障转移操作。","categories":[],"tags":[{"name":"redis","slug":"redis","permalink":"https://firecarrrr.github.io/tags/redis/"}]},{"title":"并发容器","slug":"并发容器","date":"2020-07-04T12:01:03.000Z","updated":"2020-07-13T11:54:52.304Z","comments":true,"path":"2020/07/04/并发容器/","link":"","permalink":"https://firecarrrr.github.io/2020/07/04/并发容器/","excerpt":"","text":"JUC 中的并发容器 ListList 只有一个实现 —— CopyOnWriteArrayList COWCopyOnWrite 是一种并发编程的设计模式,即写时复制。当发生写操作时,线程不是在原共享资源上进行增删操作,而是生成一个共享资源的副本在副本上增删。 从 CopyOnWriteArrayList 的源码中可以看出,写操作加了锁,进行数组的复制操作,读操作完全是无锁的。 1234567891011121314151617181920212223242526272829/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); }}/** * {@inheritDoc} * * @throws IndexOutOfBoundsException {@inheritDoc} */public E get(int index) { return get(getArray(), index);} COW 的局限性: COW 仅适用于读多写很少并且数据量不大的场景,否则资源复制会产生很大开销。 MapMap 有两个实现: ConcurrentHashMap ConcurrentSkipListMap ConcurrentHashMap 的 key 是无序的,ConcurrentSkipListMap 的 key 是有序的 ConcurrentHashMapHashTable 的实现有什么问题?HashTable 只是一个简单的并发容器的实现,只是在 get、put 等方法上面加上 synchronized 关键字,这是粒度非常大的锁,几乎所有并发操作都变成串行的了。 ConcurrentHashMap 的实现在 JDK 7 中,ConcurrentHashMap 采用分段锁机制实现: 分段锁,内部进行分段(Segment),里面存放 HashEntry 的数组,hash 相同的条目也是以链表的形式存放 img 分段数量有 concurrencyLevel 决定,默认是 16,也可以在相应的构造函数中直接指定,但必须是 2 的幂次。 分段锁的一个副作用: 在计算 size 时,如果不对所有 segment 进行同步,可能因为并发的 put 造成结果不准确。但是直接锁定所有 segment 进行计算,又会是计算的成本很高。 ConcurrentHashMap 的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数 2),来试图获取可靠值。如果没有监控到发生变化(通过对比 Segment.modCount),就直接返回,否则获取锁进行操作。 JDK 8 中的实现: 锁的粒度进一步降低,锁的是桶中的头元素 利用 CAS 等操作,在特定场景下进行无锁操作 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // lazy-load 初始化桶数组 if (tab == null || (n = tab.length) == 0) tab = initTable(); // 如果对应桶数据下标位置还没有元素 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 利用 CAS 指令,把这个值到对应桶下标处 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; // 锁定那一行 synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } // 超多阈值,进行树化 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null;} 12345678910111213141516171819202122232425262728/** * Initializes table, using the size recorded in sizeCtl. */private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // sizeCtl 是一个 volatile 的变量 // 如果 -1 表示正在被初始化,说明当前线程竞争失败,其它线程已经在初始化容器了 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin // 如果 CAS 修改 ctl 成功,进入初始化逻辑 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings(\"unchecked\") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab;} ConcurrentSkipListMap基于跳表的实现,跳表的插入、删除、查询操作的平均时间复杂度都是 logn SetSet 有两个实现: CopyOnWriteArraySet ConcurrentSkipListSet QueueQueue 的实现比较多,可以通过两个维度来区分: 是否阻塞(队列已满时,入队操作是否阻塞;队列为空时出队操作是否阻塞) 单端还是双端 单端阻塞: ArrayBlockingQueue:基于数组实现 LinkedBlockingQueue:基于链表实现 SynchronousQueue:内部没有队列的存储,每次 put 都要等待 take,每次 take 都要等待 put,也可以用于线程同步 LinkedTransferQueue PriorityBlockingQueue:无界优先队列 DelayQueue 双端阻塞: LinkedBlockingDeque 单端非阻塞: ConcurrentLinkedQueue 双端非阻塞: ConcurrentLinkedDeque 哪些是有界的,哪些是无界的?只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的 在使用其它无界队列时,一定要充分考虑是否存在 OOM 的隐患 Blocking 和 Concurrent 实现上的区别?Blocking 阻塞队列,利用锁机制实现。例如 LinkedBlockingQueue 的实现中: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576/** Lock held by take, poll, etc */private final ReentrantLock takeLock = new ReentrantLock();/** Wait queue for waiting takes */private final Condition notEmpty = takeLock.newCondition();/** Lock held by put, offer, etc */private final ReentrantLock putLock = new ReentrantLock();/** Wait queue for waiting puts */private final Condition notFull = putLock.newCondition();/** * Inserts the specified element at the tail of this queue, waiting if * necessary for space to become available. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ // 如果队列已满,阻塞 while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); // 唤醒等待的写线程 if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty();}public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { // 队列空的 while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); // 唤醒等待的读线程 if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x;} 而 ConcurrentLinkedQueue 的实现中: 12345678910111213141516171819202122232425262728293031323334353637/** * Inserts the specified element at the tail of this queue. * As the queue is unbounded, this method will never return {@code false}. * * @return {@code true} (as specified by {@link Queue#offer}) * @throws NullPointerException if the specified element is null */public boolean offer(E e) { checkNotNull(e); final Node<E> newNode = new Node<E>(e); for (Node<E> t = tail, p = t;;) { Node<E> q = p.next; if (q == null) { // 如果 p 是最后一个节点,利用 cas 指令在 p 的后面加上新节点 if (p.casNext(null, newNode)) { // Successful CAS is the linearization point // for e to become an element of this queue, // and for newNode to become \"live\". // 改变尾节点 if (p != t) // hop two nodes at a time casTail(t, newNode); // Failure is OK. return true; } // Lost CAS race to another thread; re-read next } else if (p == q) // We have fallen off list. If tail is unchanged, it // will also be off-list, in which case we need to // jump to head, from which all live nodes are always // reachable. Else the new tail is a better bet. p = (t != (t = tail)) ? t : head; else // Check for tail updates after two hops. p = (p != t && t != (t = tail)) ? t : q; }} ConcurrentLinkedQueue 是基于 CAS 的无锁技术实现的,性能更好。 fail-fast 机制Concurrent 类提供的是较低的遍历一致性。当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。","categories":[],"tags":[]},{"title":"AQS","slug":"AQS","date":"2020-07-03T07:33:29.000Z","updated":"2020-07-03T07:34:49.386Z","comments":true,"path":"2020/07/03/AQS/","link":"","permalink":"https://firecarrrr.github.io/2020/07/03/AQS/","excerpt":"","text":"什么是 AQS?AbstractQueuedSynchronizer 这个类是许多同步类的基类,是一个用于构建锁和同步器的框架。AQS 解决了在实现同步器时涉及的大量细节问题。 AQS 实现AQS 框架 img AQS 原理AQS 的核心思想是: 如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态; 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。 这个机制是通过 CLH 队列的变体实现的,将暂时获取不到锁的线程加入队列中。 AQS 中的队列是一个双向链表,AQS 通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。 img AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 操作完成对 State 值的修改。 AQS 数据结构双向链表中的节点: img 线程有两种锁模式(共享、互斥): 1234/** Marker to indicate a node is waiting in shared mode */static final Node SHARED = new Node();/** Marker to indicate a node is waiting in exclusive mode */static final Node EXCLUSIVE = null; 节点的状态可以是: 1234567891011121314151617181920212223242526272829303132333435/*** Status field, taking on only the values:* SIGNAL: The successor of this node is (or will soon be)* blocked (via park), so the current node must* unpark its successor when it releases or* cancels. To avoid races, acquire methods must* first indicate they need a signal,* then retry the atomic acquire, and then,* on failure, block.* CANCELLED: This node is cancelled due to timeout or interrupt.* Nodes never leave this state. In particular,* a thread with cancelled node never again blocks.* CONDITION: This node is currently on a condition queue.* It will not be used as a sync queue node* until transferred, at which time the status* will be set to 0. (Use of this value here has* nothing to do with the other uses of the* field, but simplifies mechanics.)* PROPAGATE: A releaseShared should be propagated to other* nodes. This is set (for head node only) in* doReleaseShared to ensure propagation* continues, even if other operations have* since intervened.* 0: None of the above** The values are arranged numerically to simplify use.* Non-negative values mean that a node doesn't need to* signal. So, most code doesn't need to check for particular* values, just for sign.** The field is initialized to 0 for normal sync nodes, and* CONDITION for condition nodes. It is modified using CAS* (or when possible, unconditional volatile writes).*/volatile int waitStatus; SIGNAL:表示当前节点的下一个节点已经被阻塞或者即将被阻塞。因此当前节点释放了锁或者放弃获取锁时,如果它的 waitStatus 是 SIGNAL,它就需要唤醒它的下一个节点。 CANCELED:表示 Node 代表的线程因为超时或者中断信号,取消获锁。处于这个状态的 Node 不会再阻塞。 同步状态 StateAQS 中维护一个名为 State 的字段,用于展示当前临界资源的获锁情况。 1234/** * The synchronization state. */private volatile int state; 通过修改 State 字段表示的同步状态来实现多线程的独占模式和共享模式。 ReentrantLock 的实现ReentrantLock#lock 过程ReentrantLock 有一个抽象的静态内部类 Sync,Sync 继承自 AbstractQueuedSynchronizer。Sync 有两个子类,NonfairSync 和 FairSync。 123456789/** Synchronizer providing all implementation mechanics */private final Sync sync;/** * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */abstract static class Sync extends AbstractQueuedSynchronizer ReentrantLock 有两个构造器,无参构造器默认构造非公平锁,可以带参构造器来构造公平锁。 1234567891011121314151617/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */public ReentrantLock() { sync = new NonfairSync();}/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();} 当在非公平锁模式下,调用 ReentrantLock#lock 方法时,会被委托给 NonfairSync#lock 执行。 1234567891011121314/** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */final void lock() { // 尝试将 state 由 0 变为 1 if (compareAndSetState(0, 1)) // 将当前线程设置为独占访问资源的线程 setExclusiveOwnerThread(Thread.currentThread()); else // 如果状态没有修改成功,应该尝试再去获取锁(可能当前线程已经获锁,实现可重入性)。 // 如果获锁失败,应该将线程加入等待队列 acquire(1);} NonfairSync#lock 调用 AbstractQueuedSynchronizer#compareAndSetState 以 CAS 方式,试图将 state 从 0 变为 1。 如果状态改变成功,就将当前线程设置为能够独占访问资源的线程。 如果状态改变失败,就调用 AbstractQueuedSynchronizer#acquire 方法。 12345678910111213141516171819202122/** * Acquires in exclusive mode, ignoring interrupts. Implemented * by invoking at least once {@link #tryAcquire}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquire} until success. This method can be used * to implement method {@link Lock#lock}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. */public final void acquire(int arg) { // 如果直接尝试去获取资源失败 // 线程在阻塞队列中获取资源,一直到获取到资源后才返回。如果在等待过程中被中断过, // 返回true,否则返回 false // 线程会在获取到资源过后再返回,并自我阻塞。 // 也就是说如果线程被中断了,也会获取资源,并且不会释放。 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();} acquire 方法调用的 tryAcquire 实际上会调用 Sync#nonfairTryAcquire,tryAcquire 会再次尝试获取锁。 AbstractQueuedSynchronizer#addWaiter 会将创建节点,并将节点加入到队列中。 AbstractQueuedSynchronizer#acquireQueued 再去判断是否可以获得锁。如果不行,修改前驱节点的 waitStatus 为 SIGNAL 然后把线程挂起。 1234567891011121314151617181920212223242526/** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 获取 state 状态 int c = getState(); // 再次尝试将状态改为 1 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 实现可重入性 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; // 防止 int 越界? if (nextc < 0) // overflow throw new Error(\"Maximum lock count exceeded\"); setState(nextc); return true; } return false;} 1234567891011121314151617181920212223242526272829303132 /** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 如果 node 的前驱节点是 head,并且尝试获锁成功 if (p == head && tryAcquire(arg)) { // 把 node 节点设置为头结点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 获锁失败,判断是否需要将当前线程挂起 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }} 12345678910 /** * Convenience method to park and then check if interrupted * * @return {@code true} if interrupted */private final boolean parkAndCheckInterrupt() { // 线程在这里被挂起,不再执行,直到被唤醒。 LockSupport.park(this); return Thread.interrupted();} 上面就是整个 ReentrantLock 在非公平状态下加锁的过程。 尝试用 CAS 指令 将 AQS 的 state 由 0 改为 1。 如果尝试成功,就加锁成功。 如果尝试失败,会创建节点,并将节点加入到双向链表中。 将当前线程对应节点的前驱节点的 waitStatus 设置为 SIGNAL,阻塞当前线程。 前驱节点对应线程在释放锁或者取消获锁时,唤醒后继线程。 img ReentrantLock#unlock 过程首先会调用 AQS#release: 12345678910111213141516171819/** * Releases in exclusive mode. Implemented by unblocking one or * more threads if {@link #tryRelease} returns true. * This method can be used to implement method {@link Lock#unlock}. * * @param arg the release argument. This value is conveyed to * {@link #tryRelease} but is otherwise uninterpreted and * can represent anything you like. * @return the value returned from {@link #tryRelease} */public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false;} 又到 Sync#tryRelease,就是把 state 改成 0 123456789101112protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free;} 公平锁和非公平锁在上面非公平锁的实现中,线程会尝试用 CAS 指令直接去改 state 去获取锁,而不管队列中是都有线程在等待。 而在公平锁的实现中: 12345678910111213141516171819202122232425262728final void lock() { acquire(1);}/** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 队列中没有线程或者当前线程就是 head 后面第一个节点时,才会尝试去改状态 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error(\"Maximum lock count exceeded\"); setState(nextc); return true; } return false; }} 不会像非公平锁中,一上来就 CAS 该状态。","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]},{"title":"CountDownLatch 和 CyclicBarrier","slug":"CountDownLatch-和-CyclicBarrier","date":"2020-07-01T08:09:49.000Z","updated":"2020-07-01T08:09:49.384Z","comments":true,"path":"2020/07/01/CountDownLatch-和-CyclicBarrier/","link":"","permalink":"https://firecarrrr.github.io/2020/07/01/CountDownLatch-和-CyclicBarrier/","excerpt":"","text":"CountDownLatch A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.A CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invocations of the countDown method, after which all waiting threads are released and any subsequent invocations of await return immediately. This is a one-shot phenomenon – the count cannot be reset. If you need a version that resets the count, consider using a CyclicBarrier. CountDownLatch 是一个线程同步的工具,可以使一个或多个线程等待其它线程执行中某些操作执行完成后再执行。 CountDownLatch 的 await 方法会阻塞到 count 值被减到 0 的时候。 CountDownLatch 中的计数值,由构造方法中给定的初值初始化。CountDownLatch 只能被使用一次,count 值不能被重置。 一个例子: 12345678910111213141516Executor executor = Executors.newFixedThreadPool(2);CountDownLatch countDownLatch = new CountDownLatch(1);executor.execute(() -> { try{ countDownLatch.await(); System.out.println(\"World\"); }catch (InterruptedException e) { e.printStackTrace(); }});executor.execute(() -> { System.out.println(\"Hello\"); countDownLatch.countDown();}); 两个线程,一个打印 Hello, 一个打印 World,一定要保证先打印 Hello,再打印 World。 CyclicBarrier A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.A CyclicBarrier supports an optional Runnable command that is run once per barrier point, after the last thread in the party arrives, but before any threads are released. This barrier action is useful for updating shared-state before any of the parties continue. CyclicBarrier 也是一个线程同步工具,允许一组线程等待彼此到达一个共同的 barrier point 。 当 CyclicBarrier 计数被减到 0 后,所有等待线程被释放后,CyclicBarrier 能够重置到设置的初始值。 12345678910111213141516171819202122Executor executor = Executors.newFixedThreadPool(2);CyclicBarrier cyclicBarrier = new CyclicBarrier(2);executor.execute(() -> { System.out.println(\"Hello\"); try { cyclicBarrier.await(); }catch (Exception e) { e.printStackTrace(); } System.out.println(\"World\");});executor.execute(() -> { System.out.println(\"Hello\"); try { cyclicBarrier.await(); }catch (Exception e) { e.printStackTrace(); } System.out.println(\"World\");}); 两个线程,打印 Hello World,必须两个线程都打印了 Hello 过后,才能打印 World。 CyclicBarrier 还有一个构造方法: 123456789101112131415161718/** * Creates a new {@code CyclicBarrier} that will trip when the * given number of parties (threads) are waiting upon it, and which * will execute the given barrier action when the barrier is tripped, * performed by the last thread entering the barrier. * * @param parties the number of threads that must invoke {@link #await} * before the barrier is tripped * @param barrierAction the command to execute when the barrier is * tripped, or {@code null} if there is no action * @throws IllegalArgumentException if {@code parties} is less than 1 */public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction;} 这个方法会创建一个 CyclicBarrier,当指定数目的线程等待它是,它将会触发,当 barrier 触发时,会执行给定的 barrier 操作,由最后一个进入 barrier 的线程执行。","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]},{"title":"TCP 连接的建立和拆除","slug":"TCP-连接的建立和拆除","date":"2020-06-29T13:33:21.000Z","updated":"2020-06-29T13:34:47.325Z","comments":true,"path":"2020/06/29/TCP-连接的建立和拆除/","link":"","permalink":"https://firecarrrr.github.io/2020/06/29/TCP-连接的建立和拆除/","excerpt":"","text":"TCP 连接建立的过程三次握手 客户端向服务端发送报文,将 SYN 标志位置 1,客户端随机产生一个初始序列号 client_isn 。 服务器收到来自客户端的 SYN 报文,会为该 TCP 连接分配缓存和变量。服务器向客户端发送确认报文,SYN 标志位置 1,随机生成服务器的初始序列号 server_isn ,将首部确认号字段设置为 client_isn + 1。 收到服务器的 SYNACK 报文后,客户端为该连接分配缓存和变量。客户端向主机发送另一个报文,确认号设置为 server_isn + 1,SYN 设置为 0,序号设置为 client_isn + 1。这个报文可以携带数据。 为什么需要三次握手? 第一次报文发送,服务端如果收到了客户端的报文,此时,服务端知道客户端到服务端的连接是可用的。 第二次报文发送,如果客户端收到了服务端的报文,此时,客户端知道了客户端到服务端和服务端到客户端的连接时可用的。 第三次报文发送,如果服务端收到了连接,此时,服务端就知道服务端到客户端的连接也是可用的。客户端和服务端就都能够确认这个链路双向都是可用的。 为什么客户端和服务端的初始序列号必须随机生成?假设有三个角色 A 代表服务器,B 代表客户端,C 代表攻击者。如果以 B 的身份伪造一个请求发送给 A,A 会给 B 发送一个 SYNACK 报文,假设此时 B 被 C 搞得处于异常状态。如果这个 SYNACK 报文中的 server_isn 是固定的,那么 C 就很容易猜测出这个 server_isn,然后构造确认号发送报文给 A。 四次挥手 参与一条 TCP 连接的两个进程中任何一个都能终止该连接。当某客户打算关闭连接: 客户应用进程给服务器发送一个报文,将 FIN 标志位置 1。 服务器会送一个确认报文,ACK 标志位置 1。 服务器发送它的终止报文,FIN 标志位置 1。 客户端对服务器的终止报文进行确认,ACK 置 1。","categories":[{"name":"Basic","slug":"Basic","permalink":"https://firecarrrr.github.io/categories/Basic/"}],"tags":[{"name":"计算机网络","slug":"计算机网络","permalink":"https://firecarrrr.github.io/tags/计算机网络/"}]},{"title":"数据分片与 redis 集群","slug":"redis-集群","date":"2020-06-29T13:32:45.000Z","updated":"2020-06-29T13:35:24.480Z","comments":true,"path":"2020/06/29/redis-集群/","link":"","permalink":"https://firecarrrr.github.io/2020/06/29/redis-集群/","excerpt":"","text":"数据分片数据分片是分布式存储中最重要的问题之一,即当存储被分散到不同的节点时,如何来将数据映射到不同的节点中? 顺序分片(范围分片)最直观的一种分片方式。假设数据范围是 1100,范围分片就是 133 落到第一个节点,3466 落到第二个节点,67100 落到第三个节点。 顺序分片的有点是能做顺序访问 范围分片的问题: 面对顺序写的时候可能存在热点 数据可能是倾斜的,比如总是倾向于访问某个范围内的数据 Hash 分片Hash 分片就是把数据 Hash 一下,然后对节点数量取余得到应该放哪儿。 Hash 分片最大的问题在于当节点需要拓展时,涉及到大量的数据迁移。 一致性 Hash image-20200628221645619 假设当前有 4 个节点,把这 4 个节点放在一个环上,这个环是从 0 到 hash 函数能够生成的最大值。 每个键被 hash 后,顺时针查找最近的节点,那这个键就应该落在这个节点上。 image-20200628222036769 假设新增一个节点 5,位于节点 2 和节点 4 之间,可以看到此时,这个变更的影响范围就被局限在了很小的一段上。 redis 虚拟槽redis 集群将整个数据库分成了 16384 个槽(slot)。数据库中的每个键都属于 16384 个槽中的一个,集群中的每个节点可以处理 0 个或者 16384 个槽。 img 槽是 redis 集群管理数据的基本单位,集群伸缩就是槽和数据在节点之间的移动。 redis 集群搭建cluster meet首先需要把配置文件里的 cluster-enabled 设置为 yes 以开启集群模式。 启动节点后,不同机器上的节点之间初跑起来的时候都是独立的,需要通过 cluster meet ip port 命令来使服务器之间握手连接。 分配槽 cluster addslots [slot]","categories":[],"tags":[{"name":"redis","slug":"redis","permalink":"https://firecarrrr.github.io/tags/redis/"}]},{"title":"MySQL 慢查询","slug":"MySQL-慢查询","date":"2020-06-29T13:31:56.000Z","updated":"2020-06-29T13:35:56.678Z","comments":true,"path":"2020/06/29/MySQL-慢查询/","link":"","permalink":"https://firecarrrr.github.io/2020/06/29/MySQL-慢查询/","excerpt":"","text":"获取有性能问题的 SQL慢查询日志与慢查询日志有关的配置: slow_query_log:启动停止记录慢查询日志 slow_query_log_file:指定慢查询日志存储路径及文件 long_query_time:指定记录慢查日志 SQL 执行时间的阈值(单位:秒) log_queries_not_using_indexes:是否记录未使用索引的 SQL(和上面那个阈值无关,只要没有用到索引就会记录) 慢查询日志中记录的内容: image-20200629204739096 执行这条语句的用户信息 语句执行的时间 锁的时间 返回的数据的行数 扫描的数据的行数 这条 SQL 语句 常用慢查询分析工具如果是一个繁忙的系统,产生慢查询日志的量可能很大,可以利用一些工具对慢查询日志进行分析。 mysqldumpslowmysqldumpslow 会合并相同的语句 1mysqldumpslow -s r -t 10 slow-mysql.log -s 指定按哪种排序方式输出结果: c:总的执行次数 t:总的执行时间 l:总的锁的时间 r:总的返回的数据行数 at,al,ar:上面那些值的平均值, -t 指定 top 多少条的数据输出 image-20200629211451977","categories":[{"name":"MySQL","slug":"MySQL","permalink":"https://firecarrrr.github.io/categories/MySQL/"}],"tags":[{"name":"SQL","slug":"SQL","permalink":"https://firecarrrr.github.io/tags/SQL/"}]},{"title":"RPC 原理","slug":"RPC-原理","date":"2020-06-23T06:42:08.000Z","updated":"2020-06-27T07:32:33.200Z","comments":true,"path":"2020/06/23/RPC-原理/","link":"","permalink":"https://firecarrrr.github.io/2020/06/23/RPC-原理/","excerpt":"","text":"什么是 RPCRPC (Remote Procedure Call),远程过程调用,也就是一台机器上调用另一台机器上的函数。尽管这两个进程并不在同一个内存空间,但这个调用过程就像发生在一个进程内一样。 市面上常见的 RPC 框架包括 Dubbo、gRPC 之类的。 一个 RPC 调用的例子12345678910111213141516171819202122// 客户端@Componentpublic class HelloClient { @Reference // dubbo注解 private HelloService helloService; public String hello() { return helloService.hello(\"World\"); }}// 服务端@Service // dubbo注解@Componentpublic class HelloServiceImpl implements HelloService { @Override public String hello(String name) { return \"Hello \" + name; }} 上面是一个 Spring 和 Dubbo 配合使用的例子。 客户端通过 @ Reference 注解,获取了一个 HelloService 接口的对象。然后就可以直接调用 HelloService 的方法,但实际上这个调用的实现发生在远程主机。 在服务端,实现了 HelloService 接口,并使用 @Service 注解(dubbo 注解),在 Dubbo 框架中注册了这个实现类。 可以看出,在业务编码层面上,使用 dubbo 实现远程调用和在本地调用并没有多大区别,这是因为 dubbo 框架在背后完成了远程调用。 在客户端,业务代码得到的 HelloService 这个接口的实现,并不是服务端提供的真正的实现类 HelloServiceImpl 的一个实例,而是由 RPC 框架提供的一个代理类的实例,这个代理类被称作 ”桩(stub)“。 作为一个代理类,桩也实现了 HelloService 接口,客户端在调用 hello 方法时,实际上调用的是这个桩的 hello 方法。在这个 hello 方法中,桩会构造一个请求,包含: 请求的服务名,例如:HelloService#hello(String) 请求的所有参数,比如例子中的 name,值是 “World” 服务端收到请求后,先把请求中的服务名解析出来,然后根据服务名在服务端进程中,找到服务的提供者。找到提供者后,RPC 框架使用客户端传来的参数调用服务提供者。服务端的 RPC 框架获得返回结果之后,再将结果封装成响应,返回给客户端。 客户端收到服务端的响应后,从中解析出返回值,返回给服务调用方。这样一次 RPC 调用就完成了。 RPC 主要技术实现一个 RPC 框架,主要需要实现: 高性能网络传输 序列化与反序列化 服务路由与服务发现 关于服务路由与服务发现: 这个模块要解决的是,客户端如何知道服务端的服务地址呢?在 RPC 框架中存在一个注册中心组件,服务端的业务代码在向 RPC 框架注册服务之后,RPC 框架就会把这个服务的名称和地址发布到注册中心上。客户端的桩在调用服务之前,会向注册中心请求服务端的地址。 RPC 框架的一个 Demo这个类里面定义了 RPC 里面最重要的几个方法: 1234567891011121314151617181920212223242526272829303132333435363738394041424344/** * RPC框架对外提供的服务接口 */public interface RpcAccessPoint extends Closeable{ /** * 客户端获取远程服务的引用 * @param uri 远程服务地址 * @param serviceClass 服务的接口类的Class * @param <T> 服务接口的类型 * @return 远程服务引用 */ <T> T getRemoteService(URI uri, Class<T> serviceClass); /** * 服务端注册服务的实现实例 * @param service 实现实例 * @param serviceClass 服务的接口类的Class * @param <T> 服务接口的类型 * @return 服务地址 */ <T> URI addServiceProvider(T service, Class<T> serviceClass); /** * 获取注册中心的引用 * @param nameServiceUri 注册中心URI * @return 注册中心引用 */ default NameService getNameService(URI nameServiceUri) { Collection<NameService> nameServices = ServiceSupport.loadAll(NameService.class); for (NameService nameService : nameServices) { if(nameService.supportedSchemes().contains(nameServiceUri.getScheme())) { nameService.connect(nameServiceUri); return nameService; } } return null; } /** * 服务端启动RPC框架,监听接口,开始提供远程服务。 * @return 服务实例,用于程序停止的时候安全关闭服务。 */ Closeable startServer() throws Exception;}","categories":[{"name":"Distributed System","slug":"Distributed-System","permalink":"https://firecarrrr.github.io/categories/Distributed-System/"}],"tags":[]},{"title":"java String","slug":"java-String","date":"2020-06-22T07:20:05.000Z","updated":"2020-06-22T08:21:26.384Z","comments":true,"path":"2020/06/22/java-String/","link":"","permalink":"https://firecarrrr.github.io/2020/06/22/java-String/","excerpt":"","text":"String 对象是如何实现的JDK 7 & 8 的实现在这两个版本的 JDK 中,String 本质上是一个不可变 char[] JDK 9 的实现JDK 9 中,char[] 被改变成了 byte[]。并且新增了一个属性 coder,来表示编码格式。这是因为一个 char 字符占两个字节,在可变长编码格式中,对于只占一个字节的字符来说,相当于浪费了一个字节的空间。用 byte[] 字符和编码格式标记的组合可以提高空间利用率。 String 对象的不可变性String 这个类本身被标记为 final,表示这个类不可继承。底层的存储数组也被标记为 private final 表示不可修改。也就是说 String 对象一旦被创建成功就不能被修改了。 为什么 String 对象要被设计为不可变的? 使得 hash 值不会变更,这使得 String 类型非常适合用来做 HashMap 的 key。 可以实现字符串常量池。 三种创建 String 的方式123String str1 = \"abc\";String str2 = new String(\"abc\");String str3 = str2.intern(); String str1 = “abc”;使用这种方式创建字符串,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象的引用。否则,就会在字符串常量池中创建新的字符串。这种方式可以减少同一值的字符串对象被重复创建,节约内存。 String str2 = new String(“abc”);用这种方式创建字符串,首先在编译类文件的时候,“abc” 常量字符会被放到常量结构中。在类加载时,“abc” 将会在常量池中创建。在 new 方法被调用时,JVM 会在堆上创建一个 String 对象,String 中的数组会引用常量池中的 “abc” 对应的数组。然后 str2 会指向堆内存上这个对象的引用。 String str3 = str2.intern();如果调用 intern 方法,会先去查看字符串常量池中是否有等于该对象字符串的引用。如果有,就会返回常量池中这个字符串的引用,如果没有,就会在把这个字符串添加到常量池中。最终这个 str3 会指向常量池中的字符串引用。 一道题123str1 == str2;str2 == str3;str1 == str3; 答案应该是,false,false,true","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]},{"title":"Gap Lock","slug":"Gap-Lock","date":"2020-06-21T06:39:06.000Z","updated":"2020-07-06T12:58:49.212Z","comments":true,"path":"2020/06/21/Gap-Lock/","link":"","permalink":"https://firecarrrr.github.io/2020/06/21/Gap-Lock/","excerpt":"","text":"幻读幻读是指一个事务在进行过程中前后两次对同一范围数据的查询结果不一致,后一次出现了前一次查询没有查到的新行。这个新行指的是被新插入的新行。 幻读只会出现在 rr 隔离级别下的当前读场景,因为在 rc 隔离级别下,语义上规定的就是一个事务提交了就能被其它事务看到,就无所谓什么幻读了。 快照读与当前读快照读在 MySQL 的默认隔离级别 RR 下,一般的 select 语句完成的是基于 MVCC 实现的快照读,自然不会出现幻读的问题。 当前读当前读是指加锁的读取和 DML 操作。例如,使用下面两条查询语句可以实现当前读: select … for share (for share mode) :这条语句是给 select 查询到的行加上读锁,其它的事务可以读到这些行,但是没办法修改,直到你的事务提交。(走全表扫描也是会被锁所有行的)。如果你执行这条语句的时候,还有其它未提交的事务对这些行进行了修改,那么你的查询就必须等到那些事务提交过后再执行。 select … for update:这条语句给查询中遇到的行加上排它锁,也就是说如果走全表扫描,整个表的行都会被锁。其它事务如果在这些语句上执行更新操作或者 select … for share 会被阻塞。在某些隔离级别下读这些数据也会被阻塞。(RR 隔离级别会忽略 read view 上出现的任何记录上的锁)。 幻读的问题创建一个表: 12345678910CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`)) ENGINE=InnoDB;insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25); 有 id 和 c, d 三行。id 是主键,c 上加了索引。 如果 for update 只在 id = 5 这一行加锁: img T1 时刻,把所有 d = 5 的行加锁。查询结果应该是 (5,5,5) T2 时刻,id = 0 的那一行数据,变成了 (0,5,5) T3 时刻,查询结果应该是 (0,5,5),(5,5,5) T4 时刻,插入了一条数据 (1,1,5),又改成了(1,5,5) T5 时刻,查询结果是 (0,5,5), (1,5,5), (5,5,5) 分析一下: T1 时刻,session A 给 id = 5 这一行加了锁,并没有给 id = 0 这一行加锁,所以 T2 这个 update 语句可以执行。这样就破坏了seesion A 给所有 d = 5 的行加锁的语义。 同样的道理,session C 也破坏了 session A 的加锁语义。 所以幻读会造成语义上的问题。 幻读还可能造成数据一致性上的问题。数据一致性包括数据库内部数据状态的一致性还包括数据和日志在逻辑上的一致性。 img T1 时刻,id = 5 这一行变成了 (5,5,100),这个结果最终在 T6 时刻正式提交。 T2 时刻,id = 0 这一行变成了 (0,5,5)。 T4 时刻,表中多了一行 (1,5,5)。 看一下 binlog 中的内容 12345678910/* T2 时刻 session B 提交 */update t set d=5 where id=0; /*(0,0,5)*/update t set c=5 where id=0; /*(0,5,5)*//* T4 时刻 session C 提交 */insert into t values(1,1,5); /*(1,1,5)*/update t set c=5 where id=1; /*(1,5,5)*//* T6 时刻 session A 提交 */update t set d=100 where d=5;/*所有d=5的行,d改成100*/ 显然,通过这个 binlog 去恢复一个库或者克隆一个库,运行的结果都是和库里的数据不一致的。 即便是我们在 T1 时刻把扫描范围内遇到的行都加上锁,Session B 会被阻塞,但是 Session C 的插入语句,由于被插入语句还不存在,不存在的语句没法加锁,也就是说可以插入成功和修改成功,这样 binlog 里的顺序变成了 Session C -> Session A -> Session B。这样用 binlog 去恢复一个库,Session B 的执行结果没问题了,Session C 的结果还是会变成 (1,5,100)。这就是幻读导致的问题。 间隙锁产生幻读的原因是行锁只能锁住已经存在的行,而插入操作是在已经存在数据的”间隙“做的操作。 比如上面例子的表中,最开始插入了 6 条数据,在表的主键索引上就形成了 7 个间隙。 img 这样在执行 select * from t where d = 5 for update 的时候,就不止给数据库中已经有的 6 个记录加锁(d 上没有索引,走全表扫描),还需要同时加 7 个间隙锁。这样就能确保无法插入新的记录。 也就是说不仅给行加上了行锁,还给行两边的间隙加上了间隙锁。 间隙锁锁定的是其他事务往这个间隙中插入记录这个操作。 间隙锁和行锁合成为 next-key lock next-key-lock 是一个前开后闭的区间 我们的表 t 初始化以后,如果用 select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。 间隙锁可能导致的问题 img Session A 启动事务,由于 id = 9 这一行不存在,加上一个 (5, 10) 的间隙锁。 Session B 启动,同样加了一个 (5, 10) 的间隙锁。 Session B 执行 insert 语句,被 Session A 的间隙锁拦住。 Session A 执行 insert 语句,被 Session B 的间隙锁拦住。 形成死锁。 间隙锁增大了锁的范围,会降低并发度。","categories":[{"name":"MySQL","slug":"MySQL","permalink":"https://firecarrrr.github.io/categories/MySQL/"}],"tags":[]},{"title":"MySQL日志","slug":"MySQL日志","date":"2020-06-19T12:48:35.000Z","updated":"2020-06-20T09:33:53.768Z","comments":true,"path":"2020/06/19/MySQL日志/","link":"","permalink":"https://firecarrrr.github.io/2020/06/19/MySQL日志/","excerpt":"","text":"MySQL 日志总结一下 MySQL (innoDB 存储引擎)的三种日志。 redo log:InnoDB 特有的物理日志 bin log:Server 层的逻辑日志 undo log:存放数据被修改前的值 Buffer PoolBuffer Pool 是 InnoDB 在内存中缓存被访问表和索引的地方。Buffer Pool 可以使得经常被访问的数据直接在内存中被处理,从而加快处理速度。缓冲池以页为单位来缓存数据,在 InnoDB 中每个数据页的大小默认是 16 KB。缓冲池的实现本质上就是数据页的一个链表,通过一个变种的 LRU 算法淘汰数据页。 Change Bufferchange buffer 是 buffer pool 的一部分,用来对非唯一索引的更新操作(Insert、Update、Delete)进行缓存的数据结构。由于缓存了,就不用立马访存写盘,进行大量的随机 IO 操作。 什么时候写 change buffer? 更新操作作用的数据页没有在 buffer pool 中,就把更新操作缓存到 change buffer 中。 什么时候 merge 数据?当之后发生更新操作的页因为读操作被加载到内存中后。 redo logredo log 是 InnoDB 引擎特有的日志,是一种物理日志,记录的是“在某个数据页做了什么修改”。 MySQL 有一项叫做 WAL (Write-Ahead Logging),也就是预写日志技术。 当有记录需要更新时,InnoDB 会把更新记录写到 redo log 中,并更新内存(写读到 buffer pool 中的数据或者写 change buffer),这个更新操作就算完成了。之后系统会在适当的时间点将更新记录刷到磁盘上。 如果 buffer 中的数据还没有刷到磁盘上,系统就发生了崩溃或者断电怎么办? redo log 另外一个重要功能就是能够使系统具有 crash-safe 能力。redo log 同样在内存中存在缓存,存在先写缓存后写磁盘的问题,可以通过 innodb_flush_log_at_trx_commit 参数设置写磁盘的时机。默认值1代表每次事务提交时,都会写到磁盘。 在实现上,redo log 是固定大小的,用两个指针来表明写文件的起点和终点: redo-log.png 如上图所示,write pos 代表当前可写的位置,checkpoint 是当前要擦除的位置。如果 write pos 追上了 checkpoint ,代表 redo log 已经写满了,这时候新的更新语句不能够再执行,需要停下来先擦掉一些数据,把 checkpoint 向前推进一些,腾出可写空间。 如何推进 checkpoint ? 如果 checkpoint 被触发后,会将 buffer 中的脏数据都刷到磁盘上,这样就能保证,即时删除 redo log 上的记录也不会发生数据丢失。 binlogbinlog 是 MySQL Server 层的日志,是逻辑日志,记录的是这个语句的原始逻辑,例如”给 ID = 1 这行的字段 c 加 1“。 binlog 有三种格式: statement:记录 sql 语句 row:记录行的内容,更新前和后都有 mixed:默认用 statement,特定的时候转换为 row 在数据更新时,redo log 和 binlog 都会用到,下面看一下一条更新语句的执行过程。 更新语句: 1update Test set a = 1 where id = 2; update.png 在写 redo log 时采用了两阶段提交,为的是避免通过 redo log 恢复数据和通过 binlog 恢复数据出现不一致。如果先写 redo log,然后系统崩了,那么通过 binlog 恢复的数据就会少一个操作。反之,通过 redo log 恢复,就会少一个操作。 与 redo log 的循环写模式不同,binlog 采用追加写的模式。也就是说只要空间足够大,对数据库的所有操作,都可以被 binlog 记录下来。 如何把数据库恢复到半个月内任意一秒? 定期做系统的整库备份。 找到离要恢复时间点最近的一次备份,利用备份中的 binlog ,重放到要恢复的时间点。 同样可以通过参数 sync_binlog 来设置 binlog 的持久化策略。设置为1表示每次事务都持久化。 undo logundo log 即回滚日志,是 InnoDB 引擎提供的逻辑日志。当事务对数据库进行修改操作时,InnoDB 不仅会记录 redo log,还会生成 undo log;如果事务执行失败或者调用 rollback,导致事务回滚时,就可以利用 undo log 中的信息将数据回滚到修改之前的样子。 undo log 主要实现两个功能: 事务回滚 MVCC","categories":[{"name":"MySQL","slug":"MySQL","permalink":"https://firecarrrr.github.io/categories/MySQL/"}],"tags":[]},{"title":"Spring 事务的传播行为","slug":"Transaction","date":"2020-06-18T14:14:06.000Z","updated":"2020-06-19T03:55:49.532Z","comments":true,"path":"2020/06/18/Transaction/","link":"","permalink":"https://firecarrrr.github.io/2020/06/18/Transaction/","excerpt":"","text":"Spring 事务传播行为什么是 Spring 事务传播行为?Spring 事务传播行为是指被声明为事务的方法,嵌套到另一个方法时,事务是如何传播的。 Spring 事务的传播行为有哪些?通过实验的方式来验证一下 Spring 事务的传播行为。 将就用一下之前项目中的用户表: 123456789101112CREATE TABLE `user` ( `id` int(20) NOT NULL AUTO_INCREMENT, `nickname` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '两次 MD5', `salt` varchar(10) COLLATE utf8mb4_bin DEFAULT NULL, `head` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, `register_time` datetime DEFAULT NULL, `last_login_time` datetime DEFAULT NULL, `login_count` int(255) DEFAULT NULL, `mobile` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; PROPAGATION_REQUIRED 定义了两个方法,一个正常执行,一个抛一个异常。 12345678910111213141516@Override@Transactional(propagation = Propagation.REQUIRED)public void addUser() { User user = new User(); user.setNickname(\"小王\"); userMapper.insert(user);}@Override@Transactional(propagation = Propagation.REQUIRED)public void addUserException() { User user = new User(); user.setNickname(\"小李\"); userMapper.insert(user); throw new RuntimeException();} 定义一个外围方法,外围方法不带事务,外围方法中,分别调用这两个方法。 1234public void addUserTest(){ propaTestService.addUser(); propaTestService.addUserException();} 运行结果: 小王插入成功,小李插入失败 也就是说,两个内部方法各自执行了自己的事务。第二个方法的回滚没有对第一个方法造成影响。 再定义一个外围方法,外围方法声明事务: 12345@Transactionalpublic void addUserTest(){ propaTestService.addUser(); propaTestService.addUserException();} 运行结果: 小王和小李都插入失败 也就是说,两个内部方法处于一个事务之中,方法2的执行抛出异常造成了两个方法的回滚。 总结一下 REQUIRED 的事务传播行为: 如果当前方法有事务,就用当前方法的事务。如果没有,就用自身的事务 PROPAGATION_SUPPORTS 外围方法没有事务的情况: 小王和小李都插入成功 外围方法有事务的情况: 小王和小李都插入失败 SUPPORTS 的事务传播行为: 事务可有可无,如果当前方法有事务,就以当前外部事务执行,如果没有事务,就以非事务的方式执行 PROPAGATION_MANDATORY 外围方法没有事务的情况: 插入失败,并且抛出异常 org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation ‘mandatory’ 外围方法有事务的情况: 都插入失败 MANDATORY 的事务传播行为: 事务是必须的,外围方法没有事务就会抛异常,有事务就以当前事务执行 PROPAGATION_REQUIRES_NEW 外围方法没有事务的情况: 小王插入成功,小李插入失败 外围方法有事务的情况: 小王插入成功,小李插入失败 REQUIRES_NEW 的传播行为: 无论外围方法有没有事务,都会新开内部方法自己的事务,且事务之间互不影响 PROPAGATION_NOT_SUPPORTED 外围方法没有事务的情况: 小王、小李都插入成功 外围方法有事务的情况: 小王、小李都插入成功 NOT_SUPPORT 的传播行为: 不支持事务,以非事务的方式执行 PROPAGATION_NEVER 外围没有事务: 都插入成功 外围有事务: 抛异常 NEVER 的传播行为: 不支持事务,有事务就抛异常 PROPAGATION_NESTED 外围没有事务: 小王插入成功,小李插入失败 外围有事务: 都插入失败 现在看来和 REQUIRED 的行为很像,来看第三种情况: 123456789@Transactionalpublic void addUserTest(){ propaTestService.addUser(); try { propaTestService.addUserException(); }catch (Exception e) { System.out.println(\"回滚\"); }} 这种情况下,外部方法中把内部方法抛出的异常 catch 了,这样得到的结果是: 小王插入成功,小李插入失败 NESTED 的传播行为: 如果外围方法有事务,内部方法会启动子事务,如果外部事务没有感知到子事务抛出的异常,可以只回滚子事务","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]},{"title":"Dynamic Proxy","slug":"Dynamic-Proxy","date":"2020-05-07T10:14:40.000Z","updated":"2020-05-07T14:42:37.738Z","comments":true,"path":"2020/05/07/Dynamic-Proxy/","link":"","permalink":"https://firecarrrr.github.io/2020/05/07/Dynamic-Proxy/","excerpt":"","text":"代理模式代理模式(Proxy Design Pattern):在不改变原始类的情况下,通过代理类来给原始类添加功能。被代理类中实现的是主要的业务逻辑,代理类在主要业务逻辑的基础上加上了一些额外的与主业务逻辑关系不大的功能,比如说做一些统计、日志等。 代理模式可以通过静态也可以通过动态的方式实现。 静态代理下面是一个静态代理的例子: 1234567// 定义一个接口,包含登录和注册方法public interface IUserController { void login(); void register();} 12345678910111213// 业务类实现登录和注册逻辑public class UserController implements IUserController{ @Override public void login() { System.out.println(\"login\"); } @Override public void register() { System.out.println(\"register\"); }} 1234567891011121314151617181920212223// 代理类继承同一个接口,将具体登录行为委托给业务类实现,在业务逻辑上加上自身代理的逻辑public class UserControllerProxy implements IUserController { private UserController userController; public UserControllerProxy(UserController userController) { this.userController = userController; } @Override public void login() { // doSomething1() userController.login(); // doSomething2() } @Override public void register() { // doSomething1() userController.register(); // doSomething2() }} 静态构建代理类在复杂的业务场景下会使类的数量大幅增加,引入大量额外工作,代码重复较多,不够简洁。动态代理可以用来解决这个问题。 动态代理动态代理的实现主要有两种 JDK 的实现和 cglib 的实现。 JDK 动态代理JDK 实现中有一个接口: 1java.lang.reflect.InvocationHandler Java doc 里面这样描述这个接口: InvocationHandler is the interface implemented by the invocation handler of a proxy instance.Each proxy instance has an associated invocation handler. When a method is invoked on a proxy instance, the method invocation is encoded and dispatched to the invoke method of its invocation handler. InvacationHandler 接口被代理实例的 invocation handler 实现。每一个代理实例都有一个关联的 invocation handler。当代理对象上有方法被调用时,将对方法进行编码,并分派到调用程序的 invoke 方法。 12public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; Processes a method invocation on a proxy instance and returns the result. This method will be invoked on an invocation handler when a method is invoked on a proxy instance that it is associated with. 处理代理实例上的方法调用,并返回结果。当代理对象发生方法调用时,这个方法会被与代理对象关联的 invocation handler 调用。 proxy – 发生方法调用的实例 method – 发生调用的方法 args – 方法调用的参数 JDK 实现还有一个关键的类: 1java.lang.reflect.Proxy Proxy provides static methods for creating dynamic proxy classes and instances, and it is also the superclass of all dynamic proxy classes created by those methods. 提供创建代理类和实例的静态方法,是所有被创建代理类的父类。 123456789public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)public static InvocationHandler getInvocationHandler(Object proxy) throws IllegalArgumentException 上面这三个方法分别获取代理类、代理实例、和关联的 Invocation Handler 一个例子: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546public class UserControllerDynamicProxy implements InvocationHandler { private Object target; public UserControllerDynamicProxy(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target, args); after(); return result; } private void before() { System.out.println(\"do something before method invoke\"); } private void after() { System.out.println(\"do something after method invoke\"); } public static void main(String[] args) { // 创建要被代理的对象 UserController userController = new UserController(); // 创建 invocationHandler UserControllerDynamicProxy userControllerDynamicProxy = new UserControllerDynamicProxy(userController); // 创建代理对象,代理对象是 IUserController 类型,但不是 UserController 类型(面向接口) IUserController controller = (IUserController)Proxy.newProxyInstance(userController.getClass().getClassLoader(), userController.getClass().getInterfaces(), userControllerDynamicProxy); // 调用代理对象上的方法,引发 invoke 调用 controller.login(); }}// 调用结果:do something before method invokelogindo something after method invoke 把上面生成的代理类的字节码反编译一下,看一下代理类的结构: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384import DynamicProxy.IUserController;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;// 代理类继承了 Proxy,实现了 IUserController 接口public final class UserControllerProxy extends Proxy implements IUserController { // 被代理对象的所有方法取出来 private static Method m1; private static Method m2; private static Method m4; private static Method m0; private static Method m3; public UserControllerProxy(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } // 调用 invocationHandler 的 invoke() 方法 public final void login() throws { try { super.h.invoke(this, m4, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final void register() throws { try { super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { m1 = Class.forName(\"java.lang.Object\").getMethod(\"equals\", Class.forName(\"java.lang.Object\")); m2 = Class.forName(\"java.lang.Object\").getMethod(\"toString\"); m4 = Class.forName(\"DynamicProxy.IUserController\").getMethod(\"login\"); m0 = Class.forName(\"java.lang.Object\").getMethod(\"hashCode\"); m3 = Class.forName(\"DynamicProxy.IUserController\").getMethod(\"register\"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } }} 看起来就和静态代理区别不大,就是自动生成了代理类。也可以看出 JDK 动态代理是面向接口的,这个代理对象与被代理对象实现了相同的接口。如果被代理对象没有实现接口,那么就不能用 JDK 的动态代理来实现。 CGLIB 动态代理如果被代理对象没有实现接口,那么可以考虑使用 cglib,cglib 采用创建目标类子类的方式来实现动态代理。 JDK 动态代理与 cglib 的比较实现上: JDK 动态代理使用反射方式实现,必须要有接口的业务类才能使用。 cglib 基于 ASM(动态字节码操作框架)实现,通过生成业务类的子类来实现。 优缺点: JDK 达到最小依赖,可能比 cglib 稳定。平滑的版本升级。代码实现简单。 cglib 无需实现接口,达到无侵入。性能较高。","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]},{"title":"Spring Bean 生命周期","slug":"SpringBean","date":"2020-05-04T06:26:59.000Z","updated":"2020-05-06T13:04:25.687Z","comments":true,"path":"2020/05/04/SpringBean/","link":"","permalink":"https://firecarrrr.github.io/2020/05/04/SpringBean/","excerpt":"","text":"Spring BeanSpring Bean 是指可以被实例化、组装,并通过 Spring IoC 容器管理的对象。 BeanFactory 和 ApplicationContext BeanFactory: 基础类型的 IoC 容器,提供完整的 IoC 服务支持。 ApplicationContext: ApplicationContext 在 BeanFactory 的基础上构建,是相对比较高级的容器实现,除了拥有 BeanFactory 的所有支持,ApplicationContext 还提供了其它高级特性,比如事件发布、国际化等。 Spring Bean 生命周期配置 Spring Bean 元信息Spring Bean 元信息是指那些描述 Bean 的基本信息。 BeanDefinition 是 Spring 中定义 Bean 配置元信息的接口。BeanDefinition 元信息包括: Name:Bean的名称或者 ID Class:Bean 的全类名 Scope:作用域 Constructor arguments:构造器参数 Properties:属性 Autowiring mode:自动装配模式 lazy-initialization mode:延迟初始化模式(延迟和非延迟) Initialization method:初始化回调方法名称 Destruction method:销毁回调方法名称 定义 Spring Bean 元信息的方式有三大类: 面向资源:XML, Properties, Guava 面向注解 面向 API 对于面向资源和面向注解的方式,Spring 利用相应的解析器将其解析为 BeanDefinition。 注册 Spring Bean配置好的 BeanDefinition 需要注册到 IoC 容器中。 BeanDefinitionRegistry 是定义 Bean 注册行为的接口,DefaultListableBeanFactory 是它的一个实现。注册 BeanDefinition 时,是将 BeanName 和 BeanDefinition 保存到一个 ConcurrentHashMap 中,BeanName 以注册顺序被保存到一个 List 中。 1234/** Map of bean definition objects, keyed by bean name. */private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);/** List of bean definition names, in registration order. */private volatile List<String> beanDefinitionNames = new ArrayList<>(256); 加载 Spring Bean ClassAbstractBeanDefinition 中有一个属性时 beanClass,当 Definition 对应的 Class 还没有被加载时,beanClass 是一个 String 记录 Bean Class 的全类名,当 Bean Class 被 ClassLoader 被加载后就是 Bean Class 的 Class 对象。 12@Nullableprivate volatile Object beanClass; Spring Bean 的实例化BeanPostProcessor:BeanPostProcessor 是一个回调接口,有两个方法,允许在初始化前和后对 Bean 进行修改。BeanPostProcessor 有一些子接口,添加了在生命周期其它阶段对 Bean 的拦截操作,比如 InstantiationAwareBeanPostProcessor , DestructionAwareBeanPostProcessor 等。BeanPostProcessor 和 BeanFactory 之间是 N : 1 的关系。BeanPostProcessor 通过配置 Bean 的方式注册到 Spring 容器中。 实例化前操作:InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation 方法可以在实例化之前生成一个代理对象,之后就不会执行 BeanDefinition 中定义的实例化操作。下面这个例子中,拦截了 User 对象。 123456789@Overridepublic Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { if(ObjectUtils.nullSafeEquals(\"user\", beanName) && User.class.equals(beanClass)) { // 把配置完成的 User 替换掉 return new User(); } // 保持 Spring IoC 容器的实例化操作 return null;} 实例化:这里就是执行 Bean 构造器的,可能是执行无参构造器,也可能是执行有参构造器。 实例化后操作:InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation 方法可以在 Spring Bean 实例化后,属性被填充前被调用。如果方法返回 false,该 Bean 实例将不允许属性赋值,也就不会执行配置元信息中定义的赋值操作。如果返回 true,就会保持 Spring IoC 容器的赋值操作。 123456789101112@Overridepublic boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { if(ObjectUtils.nullSafeEquals(\"user\", beanName) && User.class.equals(bean.getClass())) { User user = (User) bean; user.setId(2L); user.setName(\"hello\"); // user 对象不允许属性赋值填入 就是不填入配置元信息中的值 return false; } // 保持 Spring IoC 容器的属性赋值操作 return true;} Spring Bean 属性赋值赋值前阶段:InstantiationAwareBeanPostProcessor#postProcessProperties 是 Spring Bean 赋值前的回调,可以修改赋值参数-值元数据。 123456789101112131415161718@Overridepublic PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { if(ObjectUtils.nullSafeEquals(\"user\", beanName) && User.class.equals(bean.getClass())) { MutablePropertyValues propertyValues; if(pvs instanceof MutablePropertyValues){ propertyValues = (MutablePropertyValues) pvs; } else { propertyValues = new MutablePropertyValues(); } if(propertyValues.contains(\"city\")){ propertyValues.removePropertyValue(\"city\"); } propertyValues.addPropertyValue(\"city\", \"chengdu\"); return propertyValues; } return null;} Bean 属性赋值: Spring Aware 接口回调:Aware 接口是一个标记接口,用于 Spring 容器回调以注入相应的组件。Aware 的子接口包括,对 Aware 接口的回调发生在 Bean 属性赋值之后: BeanNameAware BeanClassLoaderAware BeanFactoryAware EnvironmentAware EmbeddedValueResolverAware ResourceLoaderAware ApplicationEventPublisherAware MessageSourceAware ApplicationContextAware 1234567891011121314151617181920212223242526272829303132333435363738public class UserHolder implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, EnvironmentAware { private final User user; private ClassLoader classLoader; private BeanFactory beanFactory; private String beanName; private Environment environment; public UserHolder(User user) { this.user = user; } @Override public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @Override public void setBeanName(String name) { this.beanName = name; } @Override public void setEnvironment(Environment environment) { this.environment = environment; }} Spring Bean 初始化初始化前:BeanPostProcessor#postProcessBeforeInitialization 可以在 Bean 初始化前做一些操作。 12345678@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (ObjectUtils.nullSafeEquals(\"user\", beanName) && User.class.equals(bean.getClass())) { User user = (User) bean; user.setName(\"FireCar\"); } return bean;} 初始化: @PostConstruct 注解 实现 InitializingBean 接口的 afterPropertiesSet() 方法 自定义初始化方法 (init-method) 在 Spring 容器中执行的顺序也是上面这个顺序。 初始化后回调:BeanPostProcessor#postProcessAfterInitialization 在 Bean 初始化后做一些操作。 初始化完成阶段:实现 SmartInitializingSingleton#afterSingletonsInstantiated 方法 Spring Bean 销毁销毁前回调:DestructionAwareBeanPostProcessor#postProcessBeforeDestruction 销毁: @PreDestory 实现 DisposableBean 的 destroy() 方法 自定义实现 (destroy-method)","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]},{"title":"HTTPS与TLS","slug":"TLS","date":"2020-04-22T13:01:08.000Z","updated":"2020-04-22T15:18:21.653Z","comments":true,"path":"2020/04/22/TLS/","link":"","permalink":"https://firecarrrr.github.io/2020/04/22/TLS/","excerpt":"","text":"HTTPSHTTP 是基于明文传输的,不具有安全性。很容易被攻击者截获、更改。HTTPS 协议是基于 SSL/TLS 协议上的安全的传输协议。 如何定义安全? 机密性:数据是被加密的 完整性:数据是不可篡改的 身份认证:能够确定通信方的身份 机密性的实现对称加密加密和解密使用同一个密钥 对称加密.png 对称加密的问题在于无法解决密钥交换的问题,就是说怎么安全的将密钥传递给对方? 常见的对称加密算法:AES, ChaCha20等 非对称加密存在公钥(公开)和私钥(严格保密)两个密钥 加密解密过程具有单向性:公钥加密只能用私钥解密,反之亦然 非对称加密.png 常用的非对称加密算法:RSA, ECC等 混合加密对称加密无法解决密钥交换问题,非对称加密很慢 融合两种加密方式优点的解决方案是混合加密 通信开始时,先用非对称加密解决密钥交换问题 拿到密钥后,双方使用对称加密进行通信 混合加密.png 完整性的实现摘要算法就是用一个 hash 函数,把数据压缩成一个固定长度、独一无二的摘要字符串 常用的摘要算法:MD5、SHA-1、SHA-2(TLS 推荐使用) 摘要算法.png 通信一方在发送消息时,也发送消息摘要;通信另一方解密后,再算一次摘要,进行比对。一样就是完整、未经篡改的数据。 身份认证的实现数字签名利用私钥加密摘要就能实现数字签名 因为被私钥加密的摘要只能被对应的公钥解密,这就验证了消息发送者的身份 数字签名.png 数字证书和 CA如何判断公钥的来源?如何防止黑客伪造公钥? CA(Certificate Authority): 证书认证机构,作为一个第三方,用自己的私钥来给公钥签名。把公钥和相关信息一起加密达成一个包,形成了数字证书。 客户端的”证书管理器“中有”受信任的根证书颁发机构“列表。客户端会根据这张表,查看解开数字证书的公钥是否在列表之中。如果在就用它解开数字证书,如果数字证书中记录的网址与正在浏览的网址不一致,就说明证书可能被冒用,浏览器会发出警告。 TLSSSL 即安全套阶层(Secure Socket Layer),在 OSI 模型中处于第五层(会话层)。SSL 发展到 v3 时已经被证明了是一个非常好的安全通信协议,于是 IETF 把它改名为 TLS(Transport Layer Security),正式标准化,版本号从 1.0 开始算起,实际上 TLS1.0 就是 SSLv3.1。 抓包 TLS 1.2 连接过程下面抓包百度,看看连接建立过程。开始用 Chrome 访问,Chrome 似乎做了一些神奇的优化?一些包抓不到。用 FireFox 就好了。 TCP 连接建立后,浏览器首先会发送一个 Client Hello 消息。消息包括版本号、支持的密码套件、还有一个随机数。 ClientHello.jpg 服务器收到 Client Hello 后,返回一个 Server Hello。会返回版本号、从密码套件中选择一个、一个随机数。 ServerHello.jpg 可以看出百度选择了用 ECDHE 做密钥交换算法。ECDHE 是 ECC 的一个子算法,基于椭圆曲线离散对数。 服务器把证书发送给客户端。客户端可以解析证书,取出服务器的公钥。 certificate.jpg 服务端发送 Server Key Exchange 消息,发送椭圆曲线的公钥(Server Params),再加上自己的私钥签名。客户端可以验证这个消息来自于服务器。 SKexchange.jpg 发送 Server Hello Done 消息。 ServerHelloDone.png 客户端生成一个椭圆曲线公钥(Client Params),用 Client Key Exchange 消息发送给服务器。 客户端和服务器都拿到了 Client Params、Server Params,用 ECDHE 算法,算出 Pre-Master。黑客即便拿到了前面两个参数也算不出 Pre-Master(why?)。客户端和服务器用 Client Random、Server Random、Pre-Master 生成用于加密会话的主密钥 Master Secret,由于 Pre-Master 是保密的,Master Secret 也是保密的。 客户端发送 Change Cipher Spec。再发送一个 Finished 消息,把之前所有发送的数据做个摘要,再加密发送给对方做个验证。服务器同样发送 Change Cipher Spec 和 finish。双方都验证加密、解密 OK,握手结束。后面就收发被加密的 HTTP 请求和响应。 整体流程: 上面这个过程其实是单向认证的握手过程,只认证了服务器的身份(通过服务器证书),而没有认证客户端的身份。因为单向认证过后已经建立了安全通信,可以通过账号、密码来确认用户身份。 银行的 U 盾就是给用户颁发客户端证书,实现双向认证。","categories":[],"tags":[{"name":"HTTPS","slug":"HTTPS","permalink":"https://firecarrrr.github.io/tags/HTTPS/"}]},{"title":"Reactor","slug":"Reactor","date":"2020-04-15T10:51:16.000Z","updated":"2020-04-15T14:21:53.481Z","comments":true,"path":"2020/04/15/Reactor/","link":"","permalink":"https://firecarrrr.github.io/2020/04/15/Reactor/","excerpt":"","text":"Reactor 模式 NIO 这部分一直迷迷糊糊的,想借学 Reactor 这部分的时候来谈一下。 IO所谓I/O就是内存和外部设备之间拷贝数据的过程。以网络 I/O为例,也就是用户程序从网卡读取数据和向网卡写数据的过程。这个过程可以被分为两个步骤: 内核从网卡把数据拷贝到内核缓存 数据从内核读取到用户程序地址空间 各种 I/O 模型的区别在于:实现这两个步骤的方式不一样 同步阻塞 I/O 与 TPCTPC 也就是 Thread Per Connection,就是说服务器在处理客户端请求的时候,为每一个客户端分配一个线程去处理。 tpc.png 这里之所以需要为每一个请求分配单独的线程是因为 read、write 这些调用都是同步阻塞的。如果单线程,一阻塞,系统就没办法响应新的请求了。 TPC 的问题在于,创建线程和线程的上下文切换都是需要代价的。面对海量连接,这种模型是无能为力的。 123456789101112131415161718192021222324252627282930313233public class BIOServer { static class ConnectionHandler extends Thread{ private Socket socket; public ConnectionHandler(Socket socket){ this.socket = socket; } @Override public void run() { while (!Thread.currentThread().isInterrupted() && !socket.isClosed()){ // 处理读写事件 } } } public static void main(String[] args) { // 线程池 ExecutorService executor = Executors.newFixedThreadPool(100); try{ ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(9999)); // 主线程循环等待新连接到来 while (true){ Socket socket = serverSocket.accept(); // 把任务提交给线程池 executor.submit(new ConnectionHandler(socket)); } }catch (IOException e){ e.printStackTrace(); } }} Reactor 模式TPC 之所以需要创建那么多线程,是因为在执行那些 I/O 操作的时候,并不知道内核是否已经把数据准备好,只能傻等。 NIO 能够解决这个问题,只需要在 Selector 上注册我们感兴趣的事件(有新连接到来、读就绪、写就绪等)。NIO 能同时监听多个连接,当某条连接上就绪时,操作系统就会通知线程,从阻塞态返回,开始处理。 Reactor 模式就是利用 NIO 的这种 I/O 多路复用的特性来实现的更高性能的单机高并发模型。Reactor 模式又叫做 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后 Dispatch 给某个线程。 Reactor 的核心组件包括 Reactor 和处理资源池,典型的配置方案包括: 单 Reactor / 单线程 单 Reactor / 多线程 多 Reactor / 多线程 单 Reactor / 单线程Redis 就是典型的单 Reactor / 单线程模型 单Reactor/单线程.png 客户端发来请求,Reactor 对象通过 Select 监听到连接事件。收到事件后,如果是建立连接的事件,则交给 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 处理后续时间。如果不是连接建立时间则调用 Acceptor 中建立的 Handler 来进行响应。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970public class OneThreadReactor extends Thread{ /** * 存储连接和 Handler 之间的关系 */ private HashMap<SocketChannel, ChannelHandler> handlerHashMap = new HashMap<>(); @Override public void run() { try(Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()){ serverSocketChannel.bind(new InetSocketAddress(9999)); // 设置 channel 为非阻塞 serverSocketChannel.configureBlocking(false); // 注册到 selector, 设置关注状态 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while(true) { // 阻塞等待就绪的 channel selector.select(); // 获取所有就绪的 channel Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 接受连接并注册 handler if(key.isAcceptable()) { registerHandler((ServerSocketChannel) key.channel(), selector); } // 调用连接对应 Handler 处理读事件 else if(key.isReadable()) { getHandler((SocketChannel) key.channel()).channelReadable(); } iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } /** * 接受连接并绑定处理器 * @param serverSocketChannel */ private void registerHandler(ServerSocketChannel serverSocketChannel, Selector selector){ try{ SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); handlerHashMap.put(socketChannel, new ChannelHandler(socketChannel)); } catch (IOException e){ e.printStackTrace(); } } /** * 获取绑定的 handler * @param socketChannel * @return */ private ChannelHandler getHandler(SocketChannel socketChannel){ return handlerHashMap.get(socketChannel); } public static void main(String[] args) { OneThreadReactor server = new OneThreadReactor(); server.start(); }} 单 Reactor / 单线程模型没有线程的切换,确实效率非常高,但是也有它的局限性。第一是不能利用多核能力。第二是应用场景有限,只适合处理速度非常快的场景,如果处理逻辑里有访问数据库等耗时操作,整个服务器就卡住了。 单 Reactor / 多线程 单Reactor/多线程.png 和单 Reactor / 单线程的区别是,Handler 不会负责业务处理,Handler 通过 read 读取到数据后,会发送给 Processor 进行业务处理。业务处理完成之后,会将结果发给主线程中的 send 将相应结果返回给 client。 多 Reactor / 多线程 多Reactor/多线程.png 父线程只监听连接的建立事件,当出现连接建立事件后 Acceptor 接受连接,并将新连接分配给某个子线程。子线程将监听连接,并创建一个对应的 Handler。当事件发生后,子线程调用 Handler 来做相应。 Ngnix(具体实现上有差异)、Netty 就是采用这种模型。","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]},{"title":"MySQL事务隔离和MVCC","slug":"mysql-transaction","date":"2019-08-04T08:25:20.000Z","updated":"2019-08-24T14:44:13.468Z","comments":true,"path":"2019/08/04/mysql-transaction/","link":"","permalink":"https://firecarrrr.github.io/2019/08/04/mysql-transaction/","excerpt":"","text":"MySQL事务隔离和MVCC同样,把最近看的事务和锁的部分总结一下,这部分的确东西蛮多的。 MySQL事务其实事务这东西说白了就是一组操作的集合。我们希望数据库系统能够保证这一组操作数据一致且操作独立。数据一致就是说事务在提交的时候保证事务内的所有操作都能成功完成,并且永久生效。操作独立是说多个同时执行的事务之间应该是相互独立,互不影响的。 用标准一点的说法,还就是老生常谈的ACID,感觉ACID更像是目标吧,真实的系统不一定能实现: 原子性(atomicity):这个最好理解,就是说一个事务的所有操作是不可分隔的最小工作单元,要么全部成功,要么全部失败。 一致性(consistency):数据库总是从一个一致性状态转换到另一个一致性状态。在任何时刻,包括数据库正常提供服务的时候,数据库从异常中恢复过来的时候,数据都是一致的,保证不会读到中间状态的数据。每本数据库书里都会举的转账的例子,A转200块给B的这个转账的事务执行后,A的钱少了200,B的钱就一定会多200。数据库不会因为这个事务的执行,出现逻辑上不一致的状况。 隔离性(isolation):所有事情当出现在并发的场景下都会变得复杂。通常来说,一个事务所做的修改在最终提交之前,对其他事务是不可见的。在真实地数据库系统中,不尽然是这样的,可以通过设置不同的隔离级别(isolation level)来调整不同事务操作对彼此的可见性。 持久性(durability):一旦事务提交,其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。 并不是所有存储引擎都支持事务,比如MyISAM就不支持事务。 隔离性与隔离级别当数据库中有多个事务同时执行的时候,如果不加控制,就会出现各种各样的问题: 脏读(dirty read):当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中的时候,另一个事务也访问了这个数据(脏数据)。 不可重复读(non-repeatable read):是指在一个事务中,多次读同一个数据。两次读取之间,另一个事务修改了数据,造成第一个事务两次读取数据不一致的情况。 幻读(phantom read):当保证了可重复读后,事务进行过程中查询的结果都是事务开始时的状态。但是如果另一个事务提交了数据,本事务再更新时,就可能会出现错误,就好像之前读到的数据是“鬼影”一样的幻觉似的。比如,第一个事务对一个表中的数据进行了修改或读取,这种修改或读取涉及到表中所有的数据行。同时,第二个事务向表中插入了一行新数据,就会发生第一个事务发现有行没有被修改的情况。 为了解决这些问题,就有了隔离级别的概念,SQL的标准隔离级别有: 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被其它事务看到(这不就相当于没隔离吗),所有的问题都可能发生。 读提交(read committed):一个事务提交之后,它做的变更才能被其它事务看到。读提交可以解决脏读的问题。 可重复读(repeatable read):一个事务在执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。可重复读解决了脏读和不可重复读的问题但是没有解决幻读的问题。可重复读是MySQL的默认事务隔离级别。 串行化(serializable):强制让事务串行执行,最高的隔离级别。 随着隔离级别的提升,出现并发问题的可能性在减小,可是并发性能也在降低。很多时候都是个权衡的问题。 多版本并发控制——MVCCMVCC是一种提高并发的技术。在最早的数据库系统中,只有读读之间可以并发,读写,写读,写写都是要阻塞的。引入MVCC之后,只有写写之间是相互阻塞的,其它三种操作都可以并行,大幅提高了InnoDB的并发度。在多事务环境下,对数据读写在不加读写锁的情况下实现互补干扰,从而实现了数据库的隔离性。 MVCC的实现Undo Logundo log主要用于存放数据被修改前的值。undo log分为两类,一种是INSERT_UNDO,记录插入的唯一键值;一种是UPDATE_UNDO,包括UPDATE及DELETE操作,记录修改的唯一键值以及old column记录。undo log主要由两个作用: 事务回滚 MVCC多版本控制 InnoDB以聚簇索引的方式存储数据,MySQL默认为每个索引添加了4个隐藏的字段。 其中 DB_ROLL_PTR:是undo log的指针,用于记录之前的历史数据在undo log中的位置。undo log中的历史数据行中的DB_ROLL_PTR指向的是上一次修改的行的undo log。这样多次更新后,回滚指针会把不同的版本的记录串在一起。 DB_ROW_ID:是如果没有定义主键和合适的键做主键的时候,MySQL自己生成的一个隐藏的主键。 DB_TRX_ID:是最近更改改行数据的事务ID。 DELETE_BIT:是索引删除标志,如果DB删除了一条数据,是优先通知索引将这个标志位设置为1,然后通过清除线程去异步删除真实的数据 索引隐藏字段.png 整个innoDB的MVCC机制都是通过DB_ROLL_PTR和DB_TRX_ID这两个字段实现的。 Read View由于undo log的存在,数据的多个版本得以保存。这就给事务隔离的实现创造了条件,对于RR和RC隔离级别的事务,对数据进行访问之前都需要对数据的可见性进行判断,也就是说需要判断当前事务是否能看到这一行数据,看到的应该是哪个版本的数据。这些功能的实现依赖于Read View对象。 首先,当一个事务开始的时候,会将当前数据库中正在活跃的所有事务(执行begin,但是还没有commit的事务)保存到一个叫做trx_sys的事务链表中,事务链表中保存的都是未提交的事务,当事务提交之后会从中删除。 活跃事务链表.png Read View的初始化相当于给当前的trx_sys 打一个快照。 活跃事务链表(trx_sys)中事务id最大的值被赋值给m_low_limit_id。 活跃事务链表中第一个值(也就是事务id最小)被赋值给m_up_limit_id。 m_ids 为事务链表。 12345678910111213// readview 初始化// m_low_limit_id = trx_sys->max_trx_id; // m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;ReadView::ReadView() : m_low_limit_id(), m_up_limit_id(), m_creator_trx_id(), m_ids(), m_low_limit_no(){ ut_d(::memset(&m_view_list, 0x0, sizeof(m_view_list)));} 通过这个Read View,事务就可以根据查询到的所有记录的DB_TRX_ID来匹配是否能看见该记录。 当检索到的数据的事务ID(数据事务ID < m_up_limit_id) 小于事务链表中的最小值表示这个数据在当前事务开启前就已经被其他事务修改过了,所以是可见的。 当检索到的数据的事务ID(数据事务ID = m_creator_trx_id) 表示是当前事务自己修改的数据。 当检索到的数据的事务ID(数据事务ID >= m_low_limit_id) 大于事务链表中的最大值表示这个数据在当前事务开启之前又被其他的事务修改过,那么就是不可见的。 如果事务ID落在(m_up_limit_id,m_low_limit_id),需要在活跃读写事务数组查找事务ID是否存在,如果存在,记录对于当前read view是不可见的。 如果记录对于view不可见,需要通过记录的DB_ROLL_PTR指针遍历history list构造当前view可见版本数据。 上面基本上是对于聚簇索引的情况,对于二级索引,在二级索引页中存储了更新当前页的最大事务ID,如果该事务ID大于read view中的m_up_limit_id,那么就需要回聚簇索引判断记录可见性。 RR隔离级别(除了Gap锁之外)和RC隔离级别的差别是创建read view的时机不同。RR隔离级别是在事务开始时刻,确切地说是第一个读操作创建read view的;RC隔离级别是在语句开始时刻创建read view的。 undo log什么时候会被删除呢? 当该undo log关联的事务没有出现在其他事务的read view中时(事务已提交,且没有其他事务依赖当前事务),那么InnoDB引擎的后台清除线程(purge线程)会进行遍历删除undo log操作。 因此长事务会导致数据库中存在大量的undo log,占用大量的存储空间。所以应该尽量避免长事务。 总结一个月一篇的速度真的是醉了。。。未完待续","categories":[{"name":"MySQL","slug":"MySQL","permalink":"https://firecarrrr.github.io/categories/MySQL/"}],"tags":[]},{"title":"MySQL索引(一)","slug":"mysql-index","date":"2019-07-20T09:11:18.000Z","updated":"2019-07-20T09:20:21.078Z","comments":true,"path":"2019/07/20/mysql-index/","link":"","permalink":"https://firecarrrr.github.io/2019/07/20/mysql-index/","excerpt":"","text":"MySQL索引最近在看数据库索引相关的内容,想写成blog,一来整理一下笔记,二来整理一下思路。 索引的目的是加快数据访问的速度,要实现这个目的需要用到一些高效的数据结构。索引是在存储引擎层实现的,不同的存储引擎可能采用不同的实现方式,用到的数据结构也不尽相同。 索引的数据结构基础B-Tree和B+ TreeMySQL的默认存储引擎InnoDB使用B+ Tree来实现索引,B+ Tree是B-Tree的一个变种,基本上大部分存储引擎都是使用B-Tree类的数据结构来实现索引的。 为什么要用B-Tree或者B-Tree产生的动机是什么? B-Tree本质上是二叉搜索树的一个推广,每一个B-Tree内部节点x有x.n个关键字,这x.n个关键字从小到大依次排列,把关键字分成了x.n+1个区间,那么x就有x.n+1个孩子节点分别存储这些区间范围内的关键字。由于n的数值可以很大,所以B-Tree的树高可以很低,树高低就意味着找到目标需要的随机IO次数少。n的值取多少合适呢? 我们存储在数据库里的数据,是存储在磁盘上的(也有可能是SSD啦,不过很贵吧),磁盘作为一种依靠磁臂在不同磁道和扇区之间机械运动读取数据的存储装置,与内存和CPU相比就很慢。要加快数据的访问速度那就要减少磁盘IO的次数。磁盘本身存储数据的最小单位是扇区(一般为512 byte),而操作系统的文件系统不是以扇区为单位来读取磁盘的,因为这太慢了,所以有了block(块)的概念,它是一个块一个块的读取的,如果要读取的数据超过一块就会触发多次IO,一个块的大小一般是4K byte。 一个B-Tree算法的运行时间主要由它执行磁盘读写的时间决定,所以,一个B-Tree节点的大小通常和一个完整的块的大小一样大。因此,磁盘块的大小限制了B-Tree节点可以含有的孩子个数。 B+ Tree是B-Tree的一个常见变种,B+Tree把所有的卫星数据(除作为键值外的其他数据)都存储在叶子节点里,也就是说非叶节点只存储键值和孩子指针,并且叶子节点之间用指针连接。 B-Tree由于它的有序性,所以增删节点,维护起来会耗费额外的资源 所以索引会提高查询效率,但是会降低写入和删除的效率 Hash表hash表没啥可说的,key-value存储方式。需要注意的是,由于hash索引不会按键值顺序存储,所以hash索引只适用于等值查询的场景,做区间查询会很慢,也没法做部分匹配。 索引的细节下面关于索引的讨论基本上都是针对于MySQL默认存储引擎InnoDB而言的。 聚簇索引与二级索引对于InnoDB而言,聚簇索引其实就是主键索引,在索引的叶子节点中,存储了包含全部数据的数据行。“聚簇”的意思是说数据行和相邻键值的数据行紧凑的存储在一起(并非一直成立)。 聚簇索引的实现同样依赖于存储引擎,并非所有存储引擎都支持聚簇索引。聚簇索引的优点显而易见,聚簇索引可以最大限度的提高IO密集型应用的性能。但是这种使用这种精巧的数据结构存储数据都会面临维护上的开销。对于聚簇索引来说: 插入新数据行的速度严重依赖于插入顺序。按照主键顺序插入到InnoDB表中速度肯定是最快的,非顺序插入不仅慢而且会导致很多磁盘碎片的产生。所以一般尽量用自增主键做主键值,这样在性能上和存储空间上都有优势。 更新主键的代价很大。因为是数据行按主键顺序紧密存储的,所以更新主键就会带来数据行的移动。 插入新行(乱序)、主键更新需要移动行时,都可能面临“页分裂(page split)”问题。当需要把一行插入到一个已满页面的时候,存储引擎会把这个页分裂成两个页来容纳这个行,页分裂操作会导致占用更多磁盘空间,空间利用率降低。 聚簇索引会让全表扫描变慢,尤其是行比较稀疏的时候,或者由于页分裂导致数据存储不连续的时候。这应该是和把所有数据行直接连续存储相比而言的。 二级索引就是指非主键索引,二级索引的叶子节点中除了存储索引列的值之外,还存储了对应行的主键值。这是与MyISAM存储引擎的一个明显的不同,MyISAM索引的叶子节点中存储的时指向数据行的行指针(MyISAM存储引擎按照数据的插入顺序,将数据行存储在磁盘上)。 InnoDB这种存储主键的方式带来了一个显而易见的好处就是减少了出现行移动或者数据页分裂时二级索引的维护工作。也带来了一个显而易见的坏处就是使用二级索引查询索引不能覆盖的列信息时,需要再到主键索引表回表查询一次。 innoDB和MyISAM索引.png 索引覆盖上面说了,二级索引的叶子节点中只存放索引列的值和主键ID,对于非索引列的查找需要回表。这会带来额外的开销。索引覆盖就是说能不能让索引把查找的target字段全部给包含了。 当发起一个索引覆盖的查询时,EXPLAIN的Extra列可以看到”Using index”信息。 explain.png 最左前缀原则在联合索引中,索引列的顺序对索引的利用率和性能上是有影响的。在一个多列B-Tree索引中,索引列的顺序决定了排序的顺序,越靠左的列排序的优先级越高,也就是说,会先按照第一列排序,在按照第二列排序,以此类推。与此同时,在索引匹配时,是从左往右匹配的。 所以在建立一个多列的联合索引时应该如何安排索引列的顺序呢? 最重要的原则就是如果通过调整顺序,可以少维护一个索引,那么这个顺序就是需要优先考虑的。评估的标准就是索引的复用能力。因为支持最左前缀,所以有了(a,b)这个联合索引之后,就不需要在a上建立索引了。 还有就是考虑空间占用的问题,假如需(name, age)的联合索引,和name和age单独的索引。因为name比age大,所以应该建立(name, age)这个顺序的索引。 索引下推假设现在有(name,age)联合索引,现在有一个需求:检索出表中名字第一个字是张,而且年龄是10岁的所有男孩 SQL语句如下: 1select * from tuser where name like '张 %' and age=10 and ismale=1; 在这条语句执行时,根据最左前缀匹配原则,这条语句在搜索树的时候只能用到”张“,找到第一个满足条件的记录 ID3。 在MySQL 5.6之前,只能从ID3开始一个个回表,到主键索引上找到数据行,再对比字段值。 索引下推1.jpg 在MySQL 5.6引入了索引下推优化(index condition pushdown),可以再索引遍历过程中,对索引包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表的次数。 索引下推2.png 总结关于索引的东西还有很多,这篇只是一些非常基础的内容。这篇blog拖了好久了,因为这段时间屁事儿太多了,真的很烦。关于索引的坑以后继续填。","categories":[{"name":"MySQL","slug":"MySQL","permalink":"https://firecarrrr.github.io/categories/MySQL/"}],"tags":[]},{"title":"终于搭好了","slug":"终于搭好了","date":"2019-06-15T08:48:18.332Z","updated":"2019-07-20T08:32:58.136Z","comments":true,"path":"2019/06/15/终于搭好了/","link":"","permalink":"https://firecarrrr.github.io/2019/06/15/终于搭好了/","excerpt":"","text":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartCreate a new post1$ hexo new \"My New Post\" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment","categories":[{"name":"Java","slug":"Java","permalink":"https://firecarrrr.github.io/categories/Java/"}],"tags":[]}]}