#####一. 框架#####
1.Spring IOC 原理
主要是利用Java反射获取对象进行组装
https://blog.csdn.net/fuzhongmin05/article/details/61614873
http://afghl.github.io/2017/06/11/java-spring-01-ioc.html
https://www.cnblogs.com/ITtangtang/p/3978349.html
2.Spring AOP 原理(Mybatis的DAO接口的原理正是如此)
JDK动态代理, CGLIB字节码生成的动态代理
3.整个SSM的原理(关注SpringMVC的原理)
https://blog.csdn.net/Dennis_Wu_/article/details/73437097
#####二. 缓存中间件#####
1.Redis(使用Redis从两个角度考虑: 性能和并发)
2.Redis 的主从复制(主从模式)
3.Redis的哨兵模式(Redis Sentinel简介) 参考博文: https://www.cnblogs.com/PatrickLiu/p/8444546.html
从Redis的2.8版本之后哨兵模式才稳定下来. 同时无论是哨兵模式还是主从模式, 都有一个问题就是不能水平扩容, 并且这两种模式的高可用特性都会受到Master主节点内存的限制. 还有一点, 实现哨兵模式配置不简单, 甚至可以说有些繁琐.
简介:
Sentinel(哨兵)进程是用于监控Redis集群中Master主服务器的工作状态, 在Master主服务器发生故障的时候, 可以实现Master和Slave服务器的切换, 保证系统的高可用. 哨兵是一个分布式系统, 可以在一个架构中运行多个哨兵进程, 在这些进程中使用留言协议来接收关于Master主服务器是否下线的消息, 并使用投票协议来决定是否执行自动故障转移, 以及选择哪个Slave作为新的Master. 每个哨兵进程会向其他哨兵, Master, Slave定时发送消息, 以确认对方是否活着, 如果发现对方在指定的配置时间内(可配置)未得到回应, 则暂时认为对方已掉线, 也就是所谓的"主观认为宕机"(Subjective Down). 当然有主观认为宕机就有客观认为宕机. 当哨兵群中多数Sentinel进程在Master主服务器做出SDOWN的判断, 并且通过Sentinel is-master-down-by-addr 命令交流之后, 得出Master Server下线判断, 这种方式就是"客观宕机"(Objectively). 通过一定的Vote算法, 从剩下的Slave从服务器中, 选一台作为提升Master服务器节点. 然后自动修改相关配置, 并开启故障转移(failover).
哨兵虽然有一个单独的可执行文件Redis-sentinel, 但它实际上只是一个运行在特殊模式下的Redis服务器, 你可以启动一个普通的Redis服务器时通过设定–sentinel选项来启动哨兵, 哨兵的设计思路和Zookeeper非常类似.
Sentinel(哨兵)集群之间会互相通信, 沟通交流Redis节点的状态, 做出相应的判断并进行处理, 这里的主观下线和客观下线状态是比较重要的状态, 他们决定是否进行故障转移, 可以通过订阅指定的频道信息, 当服务器出现故障的时候通知管理员, 客户端可以将Sentinel看做一个只提供了订阅功能的Redis服务器, 不可以使用Publish命令向这个服务器发送命令, 但可以使用Subscribe或psubscribe命令, 通过订阅给定频道来获取相应的事件提醒. 一个频道能够接收和这个频道名字相同的事件. 比如, 名为+sdown的频道就可以接受所有实例进入主观下线(SDOWN)状态的事件
- 哨兵进程的作用
- 监控: 哨兵会不断检查Master和Slave是否运行正常
- 提醒: 当被监控某个Redis节点出现问题时, 哨兵可以通过API向管理员或者其他应用程序发送通知
- 自动故障迁移: 当一个Master不能正常工作, 哨兵会开始一次自动故障转移操作, 它会将失效的其中一个Master的其中一个Slave升级为新的Master, 并让失效的其他Slave改为复制新的Master; 客户端试图连接失效的Master时, 集群也会向客户端返回新的Master地址, 使得集群可以使用现在新的Master替换失效的Master. Master和Slave服务器切换后, Master的redis.conf, Slave的redis.conf和sentinel.conf的配置文件的内容都会发生相应的改变. 即, Master主服务器的redis.conf配置文件中多一行slaveof的配置, sentinel.conf的监控目标会随之调换.
- Sentinel进程的工作方式:
检查主观下线状态
- 每个哨兵进程以每秒钟一次的频率向整个集群中的Master主服务器, Slave从服务器以及其他Sentinel进程发送一个PING命令(通过返回的PING命令的回复判断是否在线)
- 如果一个实例距离最后一次PING命令的时间超过down-after-milliseconds(主观下线时长)选项所选定的值, 则这个实例会被Sentinel进程标记为主观下线状态
- 如果一个Master主服务器被标记为主观下线, 则在监视这个Master主服务器的所有哨兵进程需要以每秒一次的频率确认Master主服务器的确进入主观下线状态.
检查客观下线状态
- 当有足够数量(大于等于配置文件指定值)的Sentinel进程在指定时间内确认Master主服务器进入了主观下线状态, 则Master主服务器会被标记为客观下线
- 在一般情况下, 每个哨兵进程会以每10秒一次的频率先集群中的所有Master主服务器, Slave从服务器发送INFO命令
- 当Master主服务器被Sentinel进程标记为客观下线时, Sentinel进程向下线的Master主服务器的所有Slave服务器发送INFO的命令频率会从10秒一次改为每秒一次
- 若没有足够数量Sentinel进程同意Master主服务器下线, Master主服务器的客观下线状态就被移除.若Master主服务器重新向Sentinel进程发送PING命令返回有效回复, Master主服务器的主观下线状态就会被移除
选举领头Sentinel(在将一个主服务器被判断客观下线的同时, 监视这个下线主服务器的各个Sentinel会进行协商, 选举出领头的Sentinel, 由领头的Sentinel对下线的主服务器进行故障转移操作)可参考博文: http://weizijun.cn/2015/04/30/Raft协议实战之Redis Sentinel的选举Leader源码解析/
- 在一个配置纪元里面, 每个发现主服务器进入客观下线的Sentinel都会要求其他的Sentinel将自己设置为局部领头的Sentinel
- Sentinel设置局部领头Sentinel的规则是先到先得, 之后都会被拒绝
- 接收到目标Sentinel返回的命令回复之后, 会检查配置纪元与自己的配置纪元是否相同, 如果是并且选举的ID与自己的运行ID一致, 表示目标Sentinel将源Sentinel设置为局部领头Sentinel.
- 如果某个Sentinel被半数以上的Sentinel设置为局部领头Sentinel, 这个Sentinel将成为领头Sentinel.(并且在一次配置纪元里面只能设置一次局部领头Sentinel)
- 如果给定时间内没有一个Sentinel被选举为领头Sentinel, 那么各个Sentinel将在一段时间后再次选举(下次选举每个人都将自己选举为局部领头的Sentinel, 因为每个Sentinel都认为自己是最早发现客观下线状态的Sentinel, 现在就是拼网速的时候了), 直至选举出领头Sentinel为止
故障转移
- 选举新的的主服务器.
找出状态良好, 数据完整的从服务器, 并向其发送SLAVEOF no one命令. 挑选过程: (在从服务器列表中进行过滤)
- 删除处于下线或者断线的从服务器
- 删除五秒内没回复过领头的Sentinel的INFO命令的从服务器, 保证剩余的从服务器都是最近成功通信的
- 删除断开超过down-after-milliseconds * 10毫秒的从服务器. (筛选出数据比较新的从服务器)
- 之后再根据优先级排序, 选出优先级最高的(优先级等高比较偏移量(offset), 选offset高的, 如果offset还是等高, 选运行ID最小的)
- 让已下线的主服务器的属下的所有从服务器改为复制新的主服务器.
- 将已下线主服务器设置为新的主服务器的从服务器, 当旧的主服务器上线将会成为新的主服务器的从服务器.
3.Redis 逐出策略
具体的逐出策略通过maxmemory-policy指令进行配置, 主要有以下策略 :
- noeviction : 调用某些指令是返回错误(绝大多数的写指令, DEL和部分其他指令不包括)
- allkeys-lru : 对全键进行LRU
- allkeys-random : 对全键进行随机逐出
- volatile-lru : 对执行过期时间的键进行LRU
- volatile-random : 对执行过期时间的键进行随机逐出
- volatile-ttl : 对指定过期时间, 并且TTL较短的键进行逐出
补充:volatile-* 系列指令在无键值满足条件时(例如未设置过期时间),表现为 noeviction
参考博文 : https://segmentfault.com/a/1190000005103635
4.消息队列
http://www.cnblogs.com/rjzheng/p/8994962.html
Kafka常问:
解决了什么问题:
- 削峰
- 异步
- 解耦
Kafka与Redis比较的优势.(参考博文: http://www.cnblogs.com/valor-xh/p/6348009.html?utm_source=itdadao&utm_medium=referral)
- Kafka设计初衷是一个日志系统, 其队列中的数据能够持久化一段时间. 因此后来的consumer能够通过自定义的offset获取之前的消息, 而Redis不具备这样的能力
- Redis消息推送多于实时性较高的消息推送, 并保证可靠但有一些延迟. Redis-pub/sub断电就清空, 而是用Redis-list作为消息推送虽然有持久化, 也并非完全可靠不会丢
- Redis发布订阅除了表示不同的topic外, 并不支持分组. Kafka支持分组, 多个订阅者可分组, 同一个组里只有一个订阅者会收到, 这样可以作为负载均衡.
- Redis可以作为一个轻量级的消息队列, 当数据变大(超过了10K), Redis的入队慢得无法忍受, 出队依然有很好的性能.
Kafka的数据可靠性保证(参考博文: https://blog.csdn.net/lizhitao/article/details/52296102)
当Producer先Leader发送数据时, 可以通过acks参数来设置数据可靠性的级别
- 0: 不论是否写入成功, server不需要给Producer发送Response, 如果发生异常, server会终止连接, 触发Producer更新meta数据
- 1: Leader写入成功后即发送Response, 此种情况如果Leader fail, 会丢失数据
- -1: 等待所有ISR接收到消息后再给Producer发送Response, 这是最强保证仅设置acks=-1也不能保证数据不丢失, 当ISR列表中只有Leader时, 同时有可能造成数据丢失. 要保证不丢失除了设置acks=-1, 还要保证ISR的大小大于等于2, 具体参数设置:
- request.required.acks: 设置为-1等待所有ISR列表中的Replica接收到消息后才算写成功
- min.insync.replicas: 设置为大于等于2, 保证ISR至少有两个Replica Producer要在吞吐率和数据可靠性之间做一个权衡.
Kafka如何做到一致性
一致性定义: 若某条消息对Consumer可见, 那么即使Leader宕机了, 在新的Leader上数据依然可以被读到
- HighWaterMark简称HW: Partition的高水位, 取一个partition对应的ISR中最小的LEO作为HW, 消费者最多只能消费到HW所在的位置, 另外replica都有highWaterMark, leader和follower各自负责更新自己的highWaterMark, highWaterMark <= leader.LogEndOffset
- 对于Leader新写入的msg, Consumer不能立刻消费, Leader会等待该消息被所有ISR中的replica同步后, 更新HW, 此时才能被Consumer消费, 即Consumer最多只能被消费到HW位置
这样就保证了Leader Broker失效, 该消息仍然能够在新选举的Leader中获取. 对于来自内部Broker的读取请求, 没有HW的限制. 同时, Follower也会维护一份自己的HW, Follower.HW=min(Leader.HW, Follower.offset)
特点:
- 同时为发布和订阅提供了高吞吐量. (数据: Kafka每秒可以生产约25万消息50MB, 每秒可以处理55万消息110MB)
- 可进行持久化操作. 通过持久化和replication防止数据丢失
- 分布式系统, 易于向外扩展.
- 消息被处理的状态是在consumer端维护, 而不是在server端维护. 失败能自动平衡
- 支持online和offline的场景
相关概念:
- Topic: 特指Kafka处理的消息源的不同分类
- Partition: Topic物理上的分组, 一个topic可以分为多个partition, 每个partition是一个有序队列. partition中的每条数据都会被分配一个有序的id(offset)
- Message: 消息, 是通信的基本单位, 每个producer可以向一个topic发布消息
- Producer: 消息和数据生产者, 向Kafka的一个topic发布消息的过程叫做producer
- Consumers: 消息和数据消费者, 订阅topics并处理其发布的消息的过程叫做consumers.
- Broker: 缓存代理, Kafka集群中的一台或多台服务器统称broker.
Kafka如何做到高可用(参考博文:
https://www.jianshu.com/p/c987b5e055b0)Kafka的高可用拓扑架构(关键如何Kafka如何进行Leader选举)
选举算法:每个Broker都会向Zookeeper注册临时顺序节点, 注册成功的将成为Controller, Controller将负责每个Partition的Leader选举. (避免羊群效应)
每个Partition都会维护着一个ISR(in-sync replicas)列表, 这里的ISR表示所有的Replicas都跟上了Leader, 只有ISR的每个成员才有可能称为Leader的可能(选用第一个). 如果ISR至少有一个Replica则可以保证已经Commit的消息不丢失, 但如果所有某个Partition的所有Replica都宕机了, 就无法保证数据不丢失, 这种情况下只有两种情况
- 等待ISR中的任一个Replica活过来, 并且选它为leader
- 选择第一个活过来的Replica(不一定是ISR中的)作为leader
消息发送过程:
- Producer根据指定的partition方法(round-robin, hash等), 将消息发布到指定topic的partition里面
- Kafka集群收到Producer发过来的消息后, 将其持久化到硬盘, 并保留指定时长(可配置), 而不关注消息是否被消费.
- Consumer从Kafka集群pull数据, 并控制获取消息的offset
消息消费的幂等性
每个消息都有一个Offset(代表其序号), 每次consumer消费完数据之后, 消费者端可以选择自动提交offset或者手动提交offset让Kafka知其已消费该消息
Kafka的设计
- 吞吐量
为了实现高吞吐量, Kafka做了一下的设计
- 数据磁盘持久化: 消息不存在Cache中, 直接写到磁盘中, 充分利用磁盘的顺序读写性能
- zero-copy: 减少IO操作步骤(read:先拷贝到kernel模式下, 再拷贝到user模式下; write: 将user模式下拷贝到kernel模式下, 再从kernel的socket buff中copy到网卡设备中, 共四次copy, 应用程序用Zero-Copy来请求kernel直接把disk的data传输给socket, 而不是通过应用程序传输, 大大提高应用程序性能, 减少了kernel和user模式的上下文切换. 博文: https://blog.csdn.net/u013256816/article/details/52589524)
- 数据批量发送
- 数据压缩
- Topic划分为多个partition, 提高prallelism
- 负载均衡
- producer根据用户指定的算法, 将消息发送到指定的partition
- 存在多个partition, 每个partition有自己的replica, 每个replica分布在不同的Broker节点
- 多个partition需要拉取出lead partition, lead partition负责读写, 并由ZooKeeper负责fail over
- 通过Zookeeper管理Broker与Consumer的动态加入与离开
- 拉取系统
由于Kafka Broker会持久化数据, Broker没有内存压力, 因此, Consumer非常适合采取pull的方式消费数据, 具有以下的几点好处:
- 简化Kafka设计
- Consumer根据消费能力自主控制消息拉取速度
- Consumer根据自身情况自主选择消费方式, 例如批量, 重复消费, 从尾端开始消费等
- 可扩展性
当需要增加Broker节点时, 新增的Broker会向Zookeeper注册, 而Producer及Consumer会根据注册在Zookeeper上的watcher感知这些变化, 并及时做出调整.
参考博文: https://blog.csdn.net/caisini_vc/article/details/48007297
https://blog.csdn.net/u013920292/article/details/78815161
5.Kafka的消费方式, push or pull
参考博文: https://blog.csdn.net/honglei915/article/details/37564871
#####三.Linux#####
1.IO模型
https://www.jianshu.com/p/486b0965c296
https://woshijpf.github.io/linux/2017/07/10/Linux-IO模型.html
2.Linux查找哪个线程使用CPU时间最长
(1)获取对应项目的pid
ps -ef | grep java
(2)top -H -p pid 顺序不能改变
3.进程间通信的几种方式
- 信号量
- 共享内存
- 消息队列
- 套接字
- 管道
- 命名管道
#####四.Java#####
1.Java锁原理
https://blog.csdn.net/chenssy/article/details/54883355
Jdk1.6之后的锁优化
- 自旋锁, 频繁的阻塞和唤醒对CPU来说负担很重(CPU的阻塞和唤醒需要CPU从用户态转为核心态(kernel态)). JDK中默认开启, 默认自旋次数为10
- 适应自旋锁, 会根据上次自旋的结果来自调节, 如果上次自旋成功了, 那么下次自旋的次数会更多, 反之, 自旋次数减少甚至省略自旋的过程
- 锁消除, 有些情况下, JVM检测到不可能存在共享数据竞争, JVM会对这些同步锁进行锁消除. (锁消除的依据是逃逸分析的数据支持)
- 锁粗化, 我们在使用同步锁的时候的原则是, 让同步块的作用范围尽可能的小, 这样能使得同步操作数量尽可能缩小. 但是如果连续的加锁解锁连接在一起时候, 扩展成一个更大的锁性能会更好
- 偏向锁
目的是: 为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径. 因为轻量级锁的加锁解锁操作需要依赖多次的CAS原子指令, 偏向锁可以减少不必要的CAS操作.
获取锁:
- 检测Mark Word是否为可偏向状态, 即是否为偏向锁1, 锁标识位为01
- 若为可偏向状态, 则检测线程ID是否为当前线程ID, 如果是执行最后一点, 否则继续往下执行
- 如果线程不为当前线程ID, 则通过CAS操作竞争锁, 竞争成功, 则将Mark Word的线程ID替换为当前的线程ID执行最后一点, 否则继续执行
- 通过CAS竞争锁失败, 证明当前存在多线程竞争的情况, 但到达全局安全点(这个时间点上没有正在执行的代码), 获取偏向锁的线程被挂起, 偏向锁升级为轻量级锁, 然后阻塞在安全点的线程继续往下执行同步代码块
- 执行同步代码块
释放锁
偏向锁的释放采用了一种只有竞争才会释放的锁机制, 线程是不会主动的释放偏向锁, 需要等待其他线程来竞争. 偏向锁的撤销需要等待全局安全点(这个时间点上没有正在制定的代码). 步骤如下:
- 暂停拥有偏向锁的线程, 判断锁对象是否还处于被锁定的状态
- 撤销偏向锁, 恢复到无锁状态(01)或者轻量级锁的状态
轻量级锁
引入轻量级锁的主要目的是在没有多线程竞争的前提下, 减少传统重量级锁使用操作系统的互斥量产生的性能消耗. 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁, 则会尝试获取轻量级锁.
获取锁
- 获取当前对象是否处于无锁状态(hashcode, 0, 01), 若是, 则JVM首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间, 用于存储锁对象目前Mark Word的拷贝, 否则执行最后一点.
- JVM利用CAS操作尝试将对象的Mark Word更新为Lock Record的指正, 如果成功表示竞争到锁, 则将锁标志位变为00(表示当前锁处于轻量级锁状态), 执行同步操作; 如果失败执行最后一点
- 判断当前对象的Mark Word是否指向其它线程的栈帧, 如果是则表示当前线程已经持有当前对象的锁, 则直接执行同步代码块, 否则说明该锁对象已经被其他线程抢占了, 这时轻量级锁膨胀为重量级锁, 锁标志位变为10, 后面等待的线程将会进入阻塞的状态
释放锁
轻量级锁的释放也是通过CAS操作来进行的:
- 取出获取轻量级锁保存在栈帧中的数据
- 用CAS操作将取出的数据替换为当前对象的Mark Word, 如果成功, 则说明释放锁成功, 否则执行最后一点
- 如果CAS操作替换时报, 说明其他线程尝试获取该锁, 则需要释放锁的同时需要唤醒被挂起的线程.
AQS 原理
https://segmentfault.com/a/1190000008471362
https://www.cnblogs.com/waterystone/p/4920797.html
2.线程池原理
https://www.cnblogs.com/dolphin0520/p/3932921.html
种类:
- CachedThreadPool: 一个可缓存的线程池, 如果线程池的当前规模超过了处理需求时, 那么将回收空闲线程, 当需求增加时, 则可以添加新的线程, 线程池规模不存在任何的限制.
- FixedThreadPool: 一个固定大小的线程池, 提交一个任务就创建一个线程, 直到达到线程池的最大数量, 这时线程池的大小将不会再变化
- SingleThreadPool: 一个单线程的线程池, 它只有一个工作线程来执行任务, 可以确保按照任务在队列中的顺序来串行执行, 如果这个线程异常结束将创建一个新的线程来执行任务
- ScheduledThreadPool: 一个固定大小的线程池, 并且以延迟或者定时的方式来执行任务, 类似于Timer
3.泛型的优点
原理
泛型的实现是靠类型擦除技术 类型擦除是在编译期完成的 也就是在编译期 编译器会将泛型的类型参数都擦除成它的限定类型,如果没有则擦除为object类型之后在获取的时候再强制类型转换为对应的类型。 在运行期间并没有泛型的任何信息,因此也没有优化
Java 语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了。
这带来了很多好处:
1, 类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
2,消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
3, 潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
4.多线程同步的方式
- 同步方法: synchronized关键字修饰方法
- 同步代码块 synchronized(obj){}
- wait与notify
- 使用特殊领域变量(volatile)实现同步
- 使用可重入锁实现线程同步, 如ReentrantLock.
- 使用局部变量实现线程同步, 如ThreadLocal.
- 使用阻塞队列实现线程同步, 如: xxxBlockingQueue.
- 使用原子量实现线程同步, 如: AtomicInteger
#####五数据库#####
1.MySQL索引调优
https://blog.csdn.net/zhangliangzi/article/details/51366345
https://juejin.im/post/5a6873fbf265da3e393a97fa
2.索引选择B+/- 树的原因
https://segmentfault.com/a/1190000004690721
3.聚集索引和非聚集索引
https://blog.csdn.net/lisuyibmd/article/details/53004848
Innodb 使用聚集索引, 而MyISAM使用非聚集索引
4.Innodb 使用的锁
MySQL的innodb存储引擎支持行级锁,innodb的行锁是通过给索引项加锁实现的,这就意味着只有通过索引条件检索数据时,innodb才使用行锁,否则使用表锁
概况, MySQL常用的引擎有MyISAM和InnoDB, 而InnoDB是MySQL的默认引擎. MyISAM不支持行锁, 而InnoDB支持行锁和表锁. 在InnoDB中, 行锁是通过索引加载的, 即是锁是加在索引响应的行上的, 要是对应的SQL语句没有走索引, 则会进行全表扫描, 行锁则无法实现, 取而代之的是表锁
- 表锁 : 不会出现死锁, 发送锁冲突的几率高, 并发低
- 行锁: 会出现死锁, 发生锁冲突的几率低, 并发高
行锁的类型:
行锁分为 共享锁 和 排它锁
共享锁又称: 读锁. 当某一个事务对某几行上读锁时, 允许其他事务对几行进行读操作, 但不允许进行写操作, 也不允许其他事务给这几行上排他锁, 但允许上读锁.
排他锁又称: 写锁. 当一个事务对某几个上写锁时, 不允许其他事务写, 但允许读. 更不允许其他事务给这几行上任何锁, 包括写锁.
上共享锁的写法 : lock in share mode
如: select * from tab_1 where col_1 = xxx lock in share mode;
上排他锁的写法 : for update
如: select * from tab_2 where col_1 = xxx for update;
行锁的实现
注意
- 行锁必须要有索引才能实现, 否则会自动锁全表, 那么就不是行锁
- 两个事务不能锁同一个索引.
- insert, delete, update在事务中都会自动默认加上排他锁.
5.select多选MyIsam, delete, update多选择Innodb
select性能:
- 数据块, Innodb要缓存, MyIsam只缓存索引块, 这中间的换进换出少
- Innodb寻址要映射到块, 再到行, MyIsam记录直接就是文件的Offset, 定位比Innodb要快
- Innodb还要维护MVCC一致, 虽然场景没有, 但它还是要检查维护MVCC.
update性能:
- MyIsam直接是表锁, Innodb是行锁(前提是得命中索引)
delete性能
- MyIsam直接是表重建, 而Innodb只是将数据行标记为已删除并不需要进行表重建
- MySQL的覆盖索引
- 解释一: 就是select的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。
- 解释二: 索引是高效找到行的一个方法,当能通过检索索引就可以读取想要的数据,那就不需要再到数据表中读取行了。如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引。
- 解释三:是非聚集组合索引的一种形式,它包括在查询里的Select、Join和Where子句用到的所有列(即建立索引的字段正好是覆盖查询语句[select子句]与查询条件[Where子句]中所涉及的字段,也即,索引包含了查询正在查找的所有数据)。
补充 : 不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引的列,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引.
6.MySQL的主从复制
参考博文 : https://www.cnblogs.com/gl-developer/p/6170423.html
复制过程
基本过程如下:
- Slave上面的IO线程连接上Master, 并请求从指定的日志文件的指定位置(或者从慢开始的日志)之后的日志内容
- Master接受来自Slave的IO线程的请求后, 负责复制的IO线程会根据请求信息读取日志指定位置之后的日志信息, 返回给Slave的IO线程. 返回信息除了日志所包含的信息之外, 还包括本次返回的信息已经到Master端的bin-log文件名称以及bin-log的日志的位置
- Slave的IO线程接收到信息后, 将接收到的日志内容依次添加到Slave端的relay-log文件的末端, 并读取到的Master端的bin-log的文件名和位置记录到master-info文件中, 以便下次读取的时候能够清楚的告诉Master, 需要从哪个bin-log文件的哪个位置开始往后的日志内容
- Slave的Sql线程检测到relay-log新增了内容后, 会马上解析relay-log的内容称为在Master端真实执行的那些可执行的内容, 并自身执行
7.基于Mycat实现读写分离
8.explain 分析SQL语句性能
keys: 使用的索引
rows: 查询时扫描的行数(越大越不好)
9.MySQL压测工具MySQLSlap
博文: https://blog.csdn.net/lamp_yang_3533/article/details/52913589
#####计算机网络#####
1…HTTPS和HTTP区别原理
https://www.cnblogs.com/wqhwe/p/5407468.html
https://blog.csdn.net/xionghuixionghui/article/details/68569282
2.TCP黏包(参考博文: https://www.cnblogs.com/kex1n/p/6502002.html)
简述, 在流传输中, UDP不会出现黏包, 因为它有消息边界
- 发送端需要等缓冲区满了才发送出去, 造成黏包
- 接收方不及时接受缓冲区的包, 造成多个包接收.
具体:- 发送方引起的黏包是由TCP协议本身造成, TCP为了提高传输效率, 发送方往往要收集到足够多的数据后才发送一包数据. 若连续发送的数据都很少, 通常TCP会根据优化算法把这些数据合成一包 发送出去, 这样接收方就收到了黏包数据
- 接收方引起的黏包是由于接收用户进程不及时接收数据, 从而导致黏包现象. 因为数据放在系统接收缓冲区中, 用户进程从缓冲区取数据, 若下一包数据到达时前一包数据尚未被用户取走, 而下一包数据放到系统缓冲区就接到前一包数据包之后, 而用户进程根据预订的缓冲区大小从系统接收缓冲区取数据, 这样就一次取到多包数据.
黏包情况:
- 黏在一起的包都是完整的数据包
- 黏在一起的包有不完整的包
不是所有的黏包现象需要处理, 若传输的数据为不带结构的连续流数据(如文件传输), 则不必包粘粘的包分开. 但实际工程应用中大多还是带结果的数据避免黏包的几种措施
- 对于发送方黏包, 可以变成设定, TCP提供了强制数据立即传送的操作指令push, TCP收到该指令立马将本段数据发送出去,而不必等待缓冲区满.
- 对于接收方引起的黏包, 则可优化程序设计, 精简接收进程工作量, 提高接收进程优先级等措施, 及时接收数据, 从而尽量避免黏包现象
- 有接收方控制, 将一包数据按结构字段, 认为控制分为多次接收, 然后合并, 避免黏包
以上的措施都有不足
- 第一种方式虽可以避免发送方引起的黏包, 但关闭了优化的算法, 降低了网络发送效率, 影响应用程序性能
- 第二种方式只能减少黏包的可能性, 并不能完全避免黏包, 当发送频率比较高的时候, 由于网络突发可能使某个时间段数据包到达接收方较快, 虽然接收方还是可能来不及接受,导致黏包.
- 第三种方式芮然避免黏包, 但应用程序效率较低, 对实时应用不合适.
比较可行的方向是, 解决TCP无保护边界的问题. (参考博文: https://blog.csdn.net/zhangxinrun/article/details/6721427)
- 发送固定长度的消息
- 把消息的尺寸与消息一块发送
- 使用特殊标记来区分消息间隔
#####其他主题#####
https://www.cnblogs.com/chenhuan001/p/5866916.html
#####Zookeeper#####
默认选主策略(FastLeaderElection)
参考博文: http://www.cnblogs.com/leesf456/p/6107600.html
在以下两种情况下需要进行Leader选举
服务器启动时期的Leader选举:若进行Leader选举, 则至少需要两台机器, 这里选举3台机器组成的服务器集群为例. 在集群初始化阶段, 当有一台服务器Server1启动时, 其单独无法进行和完成Leader选举, 当第二台Server2时, 此时两台机器可以互相通信, 每台机器都试图找到Leader, 于是进入Leader选举, 过程如下
- 每个Server发出一个投票. 由于是初始情况. Server1和Server2都会将自己作为Leader服务器来进行投票, 每次投票都会包含所选举的服务器的myid和ZXID, 使用**(myid, ZXID)**来表示, 此时Server1的投票为(1, 0), Server2的投票为(2, 0), 然后各自将这个投票发给机器中的其他机器.
**补充:**ZXID, 能够改变Zookeeper的状态的客户端请求(create, delete和setData)将会转发给Leader服务器, Leader执行相应的操作, 并形成状态更新, 则这些更新的状态称之为 事务. Zookeeper集群以事务的方式运行, 确保所有的事务以原子的形式被执行, 同时不被其他事务所干扰. Zookeeper并不存在类似数据库的回滚操作, 它需要确保事务的每一步操作都不互相干扰.
一个ZXID(64位)分为两部分: epoch(Leader周期高32位)和counter(事务计数底32位). epoch表示当前Leader群首的周期, counter区分同一个群首周期里不同事务的先后.
myid是直接在配置文件中读取, 范围是1~255.
- 接受来自各个服务器的投票. 集群的每个服务器收到投票后, 首先判断投票的有效性, 如检查是否为本轮投票, 是否来自LOOKING状态的服务器.
- 处理投票 针对每一个投票, 服务器都需要将别人的投票和自己的投票PK, PK规则如下:
- 优先检查ZXID. ZXID比较大的服务器优先作为Leader
- 如果ZXID相同, 那么比较myid. myid较大的服务器作为Leader服务器
对于Server1而言, 它的投票是(1, 0), 接收Server2的投票为(2, 0), 首先会比较ZXID, 均为0, 再比较myid, 此时Server2的myid最大, 于是更新自己的投票为(2, 0), 然后重新投票, 对于Server2而言, 其无需更新自己的投票, 只是再次向集群中所有机器发出上一次投票的信息即可.
- 统计投票. 每次投票完, 服务器都会统计投票信息, 判断是否已经有过半的机器接收到相同的投票信息, 对于Server1, Server2而言, 都统计出集群中已经有两台服务器接受了(2, 0)的投票信息, 此时便认为已经选出Leader.
- 改变服务器状态. 一旦确定了Leader, 每个服务器就会更新自己的状态, 如果是Follower, 那么就变更为FOLLOWING, 如果是Leader就变更为LEADING
服务器运行时期的Leader选举
在Zookeeper运行期间, Leader与非Leader服务器将各司其职, 即便有非Leader服务器宕机或新加入, 此时也不会影响Leader, 但是如果一旦Leader服务器挂了, 那么整个集群将暂停对外服务, 进入新一轮Leader选举, 其过程和宕机时期的Leader选举过程基本一致. 假设正在运行的有Server1, Server2, Server3三台服务器, 当前Leader是Server2, 若某一时刻Leader挂了, 此时便开始Leader选举, 选举过程如下:
- 变更状态. Leader挂后, 余下的非Observer服务器都会将自己的状态变更为LOOKING, 然后进入Leader选举过程
- 每个Server会发出一个投票. 在运行期间, 每个Server的ZXID可能不同, 此时假定Server1的ZXID为123, Server3的ZXID为122. 在第一轮投票中, Server1和Server3都会投自己, 产生投票(1, 123), (3, 122), 然后各自将投票发给集群中的所有机器.
- 接受来自各个服务器的投票. 与启动过程相同
- 处理投票. 与启动过程相同, 此时, Server1称为Leader
- 统计投票. 与启动过程相同
#####Http Server(Http服务器)#####
1.如何避免Nginx的单点故障(DNS不做路由映射)
使用Keepalived(heartbeat+vip虚拟ip)+Nginx保证高可用
Keepalived高可用软件 :
Keepalived软件起初是专为LVS负载均衡软件设计的,用来管理并监控LVS集群系统中各个服务节点的状态,后来又加入了可以实现高可用的VRRP功能。因此,keepalived除了能够管理LVS软件外,还可以作为其他服务的高可用解决方案软件。
keepalived软件主要是通过VRRP协议实现高可用功能的。VRRP是Virtual Router Redundancy Protocol(虚拟路由冗余协议)的缩写,VRRP出现的目的就是为了解决静态路由的单点故障问题的,它能保证当个别节点宕机时,整个网络可以不间断地运行。所以,keepalived一方面具有配置管理LVS的功能,同时还具有对LVS下面节点进行健康检查的功能,另一方面也可以实现系统网络服务的高可用功能
优点 : 实现简单
缺点 : 资源利用率只有50%
适用场景 : 大型网站
Keepalived高可用故障切换转移原理 :
Keepalived高可用服务对之间的故障切换转移,是通过VRRP来实现的。在keepalived服务工作时,主Master节点会不断地向备节点发送(多播的方式)心跳消息,用来告诉备Backup节点自己还活着。当主节点发生故障时,就无法发送心跳的消息了,备节点也因此无法继续检测到来自主节点的心跳了。于是就会调用自身的接管程序,接管主节点的IP资源和服务。当主节点恢复时,备节点又会释放主节点故障时自身接管的IP资源和服务,恢复到原来的备用角色。
DNS轮询方式
原理 :
域名注册的时候对统一主机添加多条A记录, 实现DNS轮询, DNS服务器解析请求按照记录的顺序, 随机分配到不同的IP上, 实现简单的负载均衡
优点 : 成本较低, 所以一般在小型网站用的比较多.
缺点 :
- 可靠性低
假设一个域名DNS轮询多台服务器,其中的一台服务器发生故障的情况下,那么所有的访问该服务器的请求将不会有所回应。更糟糕的是,即使立即从DNS中去掉该服务器的IP,但电信、网通等宽带接入商将DNS存放在缓存中,刷新缓存可能发生在数小时或更久以后。
- 负载分配不均匀
DNS负载均衡采用的是简单的轮询算法,不能区分服务器的差异,不能做到为性能较好的服务器多分配请求
适用场景:小型网站
参考博文 : http://www.cnblogs.com/codeon/p/7344287.html
2.负载均衡策略
- 轮询
- ip_hash
- 权重(weight)
- 最短响应时间
- url_hash
3.Netty是如何处理连接请求和业务逻辑(Reactor的三种模型, 参考博文:
https://www.jianshu.com/p/1ccbc6a348db
http://ifeve.com/netty-reactor-4/)Netty使用主从Reactor的多线程模型
当一个连接到达时, Netty会注册一个channel, 然后EventLoopGroup会分配一个EventLoop绑定到这个channel, 但在这个channel的整个生命周期, 都会由绑定的这个EventLoop来为它服务, 而这个EventLoop就是一个线程.
####分布式协调组件####
1.Zookeeper
应用场景:
- 配置维护
- 域名服务
- 分布式同步
- 注册发现
- 组服务
节点类型:
- 持久: 创建之后一直存在, 除非有删除操作, 创建节点的客户端失效也不会影响此节点
- 持久顺序: 跟持久一样, 就是父节点在创建下一级子节点的时候, 会记录每个子节点创建的先后顺序, 会给子节点名加上一个数字后缀.
- 临时: 创建客户端会话失效, 节点就没了, 不能创建子节点
- 临时顺序: 如上
Zookeeper对节点的通知是永久的吗?
不是, 官方声明: 一个Watche事件是一个一次性的触发器, 当被设置了Watch的数据发生了改变的时候, 则服务器将这个改变发送给设置了Watch的客户端, 以便通知它们.
####RPC####
1.Grpc (参考博文: https://blog.csdn.net/xuduorui/article/details/78278808)
特点:
- 高性能, 跨语言(相比较rmi等)
- 基于Http2协议(相比较Http1.x)
- 基于protobuf 3.x(序列化效率相比较XML, JSON, Java序列化的效率)性能/效率高(空间和时间效率很不错)
- 基于Netty4.x (网络传输框架)
缺点:
- Grpc尚未提供连接池, 需要自行实现
- 尚未提供服务发现(可以用Zookeeper来做), 负载均衡策略
- 基于HTTP2, 绝大部分的Http Server, Nginx都尚不支持, 即Nginx不能将Grpc请求作为Http请求来负载均衡, 而是作为普通的TCP请求(nginx1.9已支持)
- Protobuf二进制的可读性差, 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)
- Dubbo
特点:
- 网络通信框架, 默认使用Netty, 除此还有mina
- 服务调用是默认是阻塞, 如果没有返回值可以异步调用
- 注册中心默认使用Zookeeper(Redis不推荐)
- 序列化默认使用Hessian, 除此还有Dubbo, FastJSON, Java自带序列化
- 服务提供者失效踢出基于Zookeeper的临时节点原理
- 采用多版本开发, 不影响旧版本
- 调用链过长可以结合zipkin实现分布式服务追踪
- 默认使用Dubbo协议
- Dubbo集群容错, 读操作建议使用Failover失败自动切换, 默认重试两次其他服务器. 写操作建议使用FailFast快速失败, 发一次调用失败就立即报错.
通讯协议
- dubbo
dubbo缺省协议采用单一长连接和异步NIO异步通讯, 适合于小数据量大并发的服务调用, 以及服务消费机器数大于服务提供者机器数的情况. 反之, Dubbo缺省协议不适合传送大数据量的服务, 比如传文件, 传视频, 除非请求量很低.
- Transporter: mina, netty, grizzy
- Serialization: dubbo, hessian2, java, json
- Dispatcher: all, direct, message, execution, connection
- ThreadPool: fixed, cached
特性:
缺省协议, 使用mina和hessian的tbremoting交互
- 连接个数: 单连接
- 连接方式: 长连接 (关于单一长连接的博文说明: https://blog.csdn.net/zgliang88/article/details/75440043/ https://blog.csdn.net/joeyon1985/article/details/51046548)
- 传输协议: TCP
- 传输方式: NIO异步传输
- 序列化: Hessian二进制序列化
- 适用范围: 传入传出参数数据包较小(建议小于100K), 消费者比提供者个数多, 单一消费者无法压满提供者, 尽量不要用dubbo协议传输大文件或超大字符串
- 使用场景: 常规远程服务调用
RMI
RMI 协议采用JDK标准的java.rmi.*实现, 采用阻塞式短连接和JDK标准序列化方式 (注意: 如果正在使用RMI提供服务给外部访问, 同时依赖了老的common-collection包的情况下, 存在反序列化安全风险)
特性:
- 连接个数: 多连接 (一个线程一个连接)
- 连接方式: 短连接
- 传输协议: TCP
- 传输方式: 同步传输
- 序列化: Java标准二进制序列化
- 适用范围: 传入传出的参数数据包大小混合, 消费者与提供者个数差不多, 可传文件
- 使用场景: 常规远程方法调用, 与原生RMI服务互操作
Hessian
Hessian协议用于集成Hessian的服务, Hessian底层采用Http通讯, 采用Servlet暴露服务, Dubbo缺省内嵌Jetty作为服务器实现. Dubbo的Hessian可以和原生的Hessian服务互操作, 即:
- 提供者用Dubbo的Hessian协议暴露服务, 消费者使用标准的Hessian接口调用
- 或者提供方法调用标准Hessian暴露服务, 消费方用Dubbo的Hessian协议调用.
特性:
- 连接个数: 多连接
- 连接方式: 短连接
- 传输协议: Http
- 传输方式: 同步传输
- 序列化: Hessian二进制序列化
- 适用范围: 传入传出参数数据包较大, 提供者比消费者个数多, 提供者压力较大, 可传文件
- 使用场景: 页面传输, 文件传输, 或与原生Hessian服务互操作
Http
基于Http表单的远程服务调用协议, 采用Spring的HTTPInvoker实现
特性:
- 连接个数: 多连接
- 连接方式: 短连接
- 传输协议: Http
- 传输方式: 同步传输
- 序列化: 表单序列化
- 使用范围: 传入传出参数数据包大小混合, 提供者比消费者个数多, 可用浏览器查看, 可用表单或URL传入参数,暂不支持传文件.
- 适合场景: 需同时给应用程序和浏览器JS使用的服务.
Webservice
基于Webservice的远程调用协议, 基于Apache CXF的frontend-simple和transport-http实现.可以和原生Webservice服务互操作, 即:
- 提供者用Dubbo的Webservice协议暴露服务, 消费者直接用标准Webservice接口调用
- 或者提供方用标准WebService暴露服务, 消费方用Dubbo的WebService协议调用.
特性:
- 连接个数: 多连接
- 连接方式: 短连接
- 传输协议: Http
- 传输方式: 同步传输
- 序列化: SOAP文本序列化
- 使用场景: 系统集成, 跨语言调用
thrift
当前dubbo支持的thrift协议是对thrift原生协议的扩展, 在原生协议的基础上添加一些额外的头信息, 比如service name, magic number等
使用dubbo thrift协议同样需要使用thrift的IDL compiler编译相应的Java代码, 后续代码会在这方面做一些增强缓存
memcached://
基于memcached实现的RPC协议
redis://
基于Redis实现的RPC协议注册中心
- Multicast注册中心
Multicast注册中心不需要启动任何中心节点, 只要广播地址一样, 就可以互相发现
- 提供方启动时广播自己的地址
- 消费方启动时广播订阅请求
- 提供方收到订阅请求时, 单播自己的地址给订阅者, 如果设置了unicast=false, 则广播给订阅者
- 消费方收到提供方地址, 连接该地址进行RPC调用
组播受网络结果限制, 只适合小规模应用或开发阶段使用. 组播地址段: 224.0.0.0 - 239.255.255.255
- Zookeeper注册中心
Zookeeper是一个树形目录结构, 支持变更推送, 适合作为Dubbo服务的注册中心, 工业强度较高,可用于生产环境, 并推荐使用.
流程说明:
- 服务提供者启动时, 向/dubbo/com.foo.BarService/providers 目录下写入自己的URL地址
- 服务消费者启动时, 订阅/dubbo/com.foo.BarService/providers 目录下的提供者的URL地址. 并向/dubbo/com.foo.BarService/consumers 目录下写入 自己的URL地址
- 监控中心启动时: 订阅/dubbo/com.foo.BaseService目录下的所有提供者和消费者URL地址.
支持以下的功能:- 当提供者出现断电等异常停机时, 注册中心能自动删除提供者信息
- 当注册中心重启时, 能自动恢复注册数据, 以及订阅请求
- 当会话过期时, 能自动恢复注册数据, 以及订阅请求
- 当设置<dubbo: registry check=“false”/>时, 记录失败注册和订阅请求, 后台定时重试
- 可通过<dubbo: registry username=“admin” password=“1234”/>设置Zookeeper登录信息
- 可通过<dubbo: registry group=“dubbo”/>设置Zookeeper的根节点, 不设置将使用无根树
- 支持*号通配符<dubbo: reference group="*" version="*"/>, 可订阅的所有分组和所有版本的提供者
3.Dubbo的原理
参考博文: https://blog.csdn.net/paul_wei2008/article/details/19355681
(通信网络框架基于Mina的Reactor模型通信, 基于tcp长连接. 通信协议使用缺省的Dubbo协议采用单一长连接和NIO异步通讯)
- Client一个线程调用远程接口, 生成一个唯一的Id(比如一段随机字符串, UUID等), Dubbo是使用AtomicLong从0开始累积数字的
- 将打包的方法调用信息(如调用的接口名称, 方法名称, 参数列表等), 和处理结果的回调对象callback, 全部封装在一起, 组成一个对象Object先专门存放调用信息的全局ConcurrentHashMap里面put(Id, object)
- 将Id和打包的方法调用信息封装成一对象connRequest , 使用IoSession.write(connRequest)异步发送出去
- 当前线程再使用callback的get()方法试图获取远程返回的结果, 在get()内部, 则使用synchronized获取回调对象的额callback对象, 再先检测是否已经获取到结果, 如果没有, 然后回调callback的wait()方法, 释放callback上的锁, 让当前线程处于等待状态.
- 服务端接受到请求并处理后, 将结果(此结果包含前面的ID, 即回传)发送给客户端, 客户端socket连接上专门监听消息的线程收到消息, 分析结果取到Id, 再从前面的ConcurrentHashMap里面get(Id), 从而找到callback, 将方法调用结果设置到callback对象里.
- 监听线程接着使用synchronized获取回调对象callback锁(因为前面调用过wait(), 那个线程已释放callback的锁), 再notifyAll(), 唤醒前面处于等待状态的线程继续执行(callback的get()方法继续执行就能拿到调用结果), 至此, 整个过程结束.
4.分布式锁
参考博客: https://blog.csdn.net/xlgen157387/article/details/79036337
为什么需要分布式锁
解决一种跨JVM的互斥资源机制来控制共享资源的访问, 这就是分布锁要解决的问题.
分布锁应该具备哪些条件
- 在分布式系统环境下, 一个方法在同一时间只能被一个机器的一个线程执行
- 高可用的获取锁与释放锁
- 具备可重入特性
- 具备锁失效机制, 防止死锁
- 具备非阻塞锁特性, 即没有获取到锁将立即返回获取锁失败.
分布式锁的三种实现方式
目前大型网站及应用都是分布式部署的, 分布式场景中的一致性问题一直是比较重要的问题. 分布式的 CAP理论告诉我们任何一个分布式系统都无法同时满足一致性(Consistency), 可用性(Availability)和分区容错性(Partition tolerance), 最多同时满足两项. 大多数场景中, 都需要牺牲强一致性来换取系统的高可用性, 系统往往只需要保证最终一致性. so, 为了保证数据的最终一致性, 需要很多技术方案来支持, 比如分布式事务, 分布式锁等, 保证一个方法在同一时间只能被同一个线程执行.
- 基于数据库实现分布式锁
- 基于缓存(Redis等)实现分布式锁
- 基于Zookeeper实现分布式锁
尽管有三种方案, 但是不同的业务需要根据自己的情况进行选型, 没有最好只有更合适.
基于数据库的实现方式
基于数据库的实现方式的核心思想是: 在数据库中创建一个表, 表中包括方法名等字段, 并在方法名字字段上创建唯一索引, 如果要执行某个方法, 就是用这个方法名向表中插入数据, 成功插入则获取锁, 执行完成后删除对应的行数据释放锁
注意: 这只是基于数据库的一种方法, 使用数据库实现分布式锁还有其他的玩法
使用基于数据库的这种方式很简单, 但是对于分布式锁应该具备的条件来说, 还有一些 问题需要解决及优化:
- 因为是基于数据库实现的, 数据库的可用性和性能将直接影响分布式锁的可用性及性能, 所以, 数据库需要双机部署, 数据同步, 主备切换
- 不具备可重入的特性, 因为同一个线程在释放锁之前, 行数据一直存在, 无法再次成功插入数据, 所以要在表中新增一列, 用于记录当前获取锁的机器和线程信息, 在再次获取锁的时候, 先查询表中机器和线程信息是否和当前机器和线程相同, 若相同则直接获取锁即可.
- 没有锁失效机制, 因为可能出现成功插入数据后, 服务器宕机了, 对应的数据没有被删除, 当服务回复后一直获取不到锁, 所以需要在表中新增一列, 用于记录失效时间, 并且需要有定时任务清除这些失效的数据
- 不具备非阻塞锁的特性, 获取不到锁直接返回失败, 所以需要优化获取逻辑, 循环多次去获取.
- 在实施过程中会遇到各种不同的问题, 为了解决这些问题, 实现的方式会越来约复杂, 依赖数据库需要一定的资源开销, 性能问题需要考虑.
基于Redis的实现方式(参考博文: https://www.cnblogs.com/linjiqin/p/8003838.html)
选用Redis实现分布式锁原因
- Redis有很高的性能
- Redis命令对此支持比较好, 实现比较方便
实现思想:
- 获取锁的时候, 使用setnx加锁, 并使用expire命令为锁添加一个超时时间, 超过改时间则自动释放锁, 锁的value值为一个随机生成的UUID, 通过此释放锁进行判断
- 获取锁的时候还设置一个获取的超时时间, 若超过这个时间则放弃获取锁
- 释放锁的时候, 通过UUID判断锁是不是该锁, 则执行delete进行锁释放.
具体实现:
加锁:
jedis.set(String key, String value, String nxxx, String expx, int time);
- 第一个为key, 我们使用key来当锁, 因为key是唯一的
- 第二个value, 传进requestId, 解锁的时候的依据, 同时可以满足可重入锁的条件.
- 第三个为nxxx, 这个填的是NX, 意思是SET IF NOT EXIST, 即当key不存在时, 我们进行set操作, 如key存在, 则不进行任何操作.
- 第四个expx, 这个参数我们传的是PX, 意思是我们给这个key加一个过期的设置, 具体时间由第五个参数决定
- 第五个time, 代表key过期时间
其他错误做法: 如先使用setnx(), 再使用expire()方法给锁加一个过期时间, 然而这连续的两个操作不是原子操作, 可能还是有死锁的可能性, 主要原因是jedis并不支持多参数的set()方法.
还有另一种错法, 就是使用setnx(), key是锁, value是过期时间, 每次通过比较时间判断是否过期. 但是隐藏有一个问题就是, 时间是客户端生成的过期时间, 所以要求分布式情况下的客户端时间必须同步. 锁过期的时候, 同时多个客户端执行, 最终只有一个客户端可以加锁, 但是这个客户端加的锁的过期时间可能被客户端覆盖. 同时所也不具备拥有者标识, 任何客户端都可以解锁解锁:
可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-下面错误示例】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性.错误实例:
如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了基于Zookeeper的实现方式
Zookeeper是一个为分布式提供一致性服务的开源组件, 内部是一个分层的文件系统目录树结构, 规定一个目录下只能有一个唯一的文件名, 基于Zookeeper实现分布式锁的步骤如下:
- 创建一个目录mylock
- 线程A想获取锁就在mylock下创建临时顺序节点;
- 获取mylock目录下所有的子节点, 然后获取比自己小的兄弟节点, 如果不存在, 则说明当前线程顺序最小, 获得锁
- 线程B获取所有节点, 判断自己不是最小节点, 设置监听比自己次小的节点
- 线程A处理完, 删除字节的节点, 线程B监听到变更事件, 判断自己是不是最小节点, 如果是则获得锁.
此处推荐一个Apache的开源库Curator, 它是一个Zookeeper的客户端, Curator提供的Int而ProcessMutex是分布式锁的实现, acquire方法用于获取锁, release方法用于释放锁.
优点: 具备高可用, 可重入, 阻塞锁特性, 可解决失效死锁问题
缺点: 因为需要频繁的创建和删除节点, 性能上不如Redis方式.
高性能异步网络传输框架Netty
1.Reactor模型
- Reactor单线程模型
这个模型NIO模型类似, 只是将相关处理独立到Handler中去.
流程:
- 服务器端的Reactor是一个线程对象, 该线程会启动事件循环, 并使用Selector来实现I/O多路复用. 注册一个Acceptor事件处理器到Reactor中, Acceptor事件处理器所关注的时间是ACCEPT事件, 这样Reactor会监听到客户端发起的连接请求事件(ACCEPT事件)
- 客户端向服务器端发起一个连接请求 , Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给响应的Acceptor处理器来处理. Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel), 然后将该连接所关注的READ时间以及对应的READ时间处理器注册到Reactor中, 这样一来Reactor就会监听该连接的READ事件了. 或者当你需要向客户端发送数据时, 就向Reactor注册该连接的WRITE事件和其处理器
- 当Reactor监听到有读或写事件发生时, 将相关的事件派发给对应的处理器进行处理.
- 每当处理完所有就绪的感兴趣的I/O事件后, Reactor线程会再次执行select()阻塞等待新的事件就绪并将其派发给对应的处理器进行处理.
注意, Reactor的单线程模式的单线程主要是针对于I/O操作而言, 也就是所有的I/O的accept(), read(), write()以及connect()操作都在一个线程上完成的
在单线程的Reactor模型中, 不仅I/O操作在该Reactor线程上, 连非I/O业务操作也在该线程上进行处理, 这回大大延迟I/O请求的响应
- Reactor多线程模型
Reactor多线程模型就是添加一个工作者线程池, 并将非I/O操作从Reactor线程中转移交给工作者线程池来执行. 提高Reactor线程的I/O响应, 不至于因为一些耗时的业务逻辑而延迟对后面的I/O请求的处理.
以上改进的版本中, 所有的I/O操作依旧由一个Reactor来完成, 包括I/O的accept(), read(), write()以及connect()操作
当用户进一步增加时, Reactor会出现瓶颈,海量消息的读取和发送压垮服务器.
在这里插入图片描述
- 主从Reactor模型
Reactor线程池中的每一Reactor线程都会有自己的Selector, 线程和分发的事件循环逻辑.
主Reactor用于响应客户端的连接请求, 然后将接收到SocketChannel传递给subReactor, subReactor用于处理IO操作请求
- 注册一个Reactor事件处理器到mainReactor中, Acceptor事件处理器所关注的事件是ACCEPT事件, 这样mainReactor会监听客户端向服务端发起的连接请求事件(ACCEPT事件). 启动mainReactor的事件循环
- 客户端向服务器发起一个连接请求, mainReactor监听到该ACCEPT事件并将该ACCEPT事件派发给Acceptor处理器来进行. Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel), 然后将这个SocketChannel传递给subReactor线程池.
- subReactor线程池分配一个subReactor线程给这个SocketChannel, 将SocketChannel关注的READ事件以及对应的READ事件处理器注册到subReactor线程中. 当然也注册到WRITE事件以及WRITE事件处理器到subReactor线程中以完成I/O写操作. Reactor线程池中的每一Reactor线程都会有自己的Selector, 线程和分发的循环逻辑.
- 当有I/O事件就绪时, 相关的subReactor就将事件派发给响应的处理器处理. 注意, 这里的subReactor只负责完成I/O的read()操作, 在读取数据将业务逻辑的处理放入到线程池中完成, 若完成业务逻辑后需要返回数据给客户端, 则相关的I/O的write操作还是会被提交到subReactor线程来完成