• 问:我们先聊一下Java基础知识吧,在你工作的时候,用过哪些并发的工具类呢?

我在工作的时候用过Atomiclnteger,AtomicReference这些原子类,也用过 ReentrantLock 、还有ReadWrtteLock这两种锁,然后还用过CountDownLatch

  • 问: 简单介绍一下 Aomiclnteger底层的原理吧

Atomiclnteger底层是通过一个volatile 修饰的lnt值还有CAS操作实现的,CAS 操作的这个含义呢就是JVM的一个原子性操作,它具体是比较一个值,和我们预期的值是不是一致的,在这个时候如果一致的话就表示没有其他线程,并发去修政这个值。这样的话我们就可以安全的把这个值替换成我们的目标值

  • 问: 为什么不能使用volatile关键字,必须使用Atomiclnteger呢?

当时我的场景是需要把一个值进行加一操作,这样的话,就必须使用Atomiclnteger,因为volatile关键字它只能保证可见性、顺序性和读写操作的原子性。我先简单说一下volatile 的这几个特性吧,可见性就是一个线程修改了volatile关键字修饰的变量之后呢,然后另一个线程就能够立刻看到这个值的变化,这个就是可见性。还有就是那个顺序性,顺序性的话呢,就是JVM或者是CPU会对指令进行重排序,这个重排序就是优化指令的执行顺序,然后在这个单线程场景里面呢,重排序可以保证执行结果的正确性的,但是在这个多线程里面发生重排序就可能会导致结果错误。如果有这个volatile关键字修饰的变量参与到这个运算里面的话,volatile 涉及到的这些命令都不会被重排序。最后一个是读写操作的原子性,这个在32位机器上读或者写一个64位值比如说一个long变量的时候,是操作32位然后再操作剩下32位的这是一个组合操作,在并发场景下呢就可能得到一个错的结果 ,volatile 修饰的变量就可以保证这种读写操作是原子的,volatile是没有办法保证读取~加一、写回这种组合操作的原子性的,所以需要使用这个AtomicInteger 原子类或是加锁

  • 问:刚才你提到 volatile 可以保证可见性,两个线程之间的修改是不可见的吗?

这主要是因为JAVA内存模型的原因,在JAVA的内存模型里面会把内存分成主内存还有工作内存,主内存是所有线程共享的这个内存区域,然后工作内存是每个线程独占的内存,然后在一个线程想去修改一个变量值的时候需要先把这个变量从主内存拷贝到自己的工作内存里面,然后线程才能进行修改操作,比如说这个加一操作吧,线程是读取不到其他线程的工作内存的,所以这个线程还需要把修改后的这个值写回到主内存当中,然后其他线程才能读到这个修改的值,然后volatile 关键字可以通过总线的一些特性,把修改之后的新值立刻写回到主内存当中,同时还会把其他线程的工作内存的这个volatile变量设置成无效,这样的话其他线程再去使用这个 volatile变量值的时候就要重新从主内存当中去读取了也就保证了可见性

  • 问:我们再聊聊锁的事情:ReentrantLock和 ReadWrfteLock这两个锁的底层原理能介绍一下吗?

ReentrantLock底层都是依赖AQS 这个抽象类实现的,AQS里面它最重要的的就是一个volatile关键字修饰的state字段,它是一个long值用来保存了当前锁的这个状态,在这个ReentrantLock 获取锁的时候,会先检查一下这个state值,如果这个值是0的话就是可以获取锁,这个时候会记录当前线程为锁的持有者,还会用CAS把state加1,后面线程就是后面其他的线程来加锁的时候呢,就会发现state的是1,因为锁已经被第一个线程持有了嘛,所以这个时候ReentrantLock会把当前线程加到一个队列里面阻塞等待锁被释放,这个地方阻塞线程使用的是 Unsafe工具类实现的,不是synchronized关键字之类的方式,释放锁的时候呢是获取锁的逆过程,就是把state 设置为减一,要是state减到0了就会释放锁,这个时候呢我们就需要用unpark方法唤醒队列里面阻塞等待的一个线程,ReadWriteLock底层也是依赖AQS实现的,ReadWriteLock是把那个state 划分成了高32位还有低32位,这里面这个高32位呢是用来表示写锁的重入次数,然后我们刚才说到的低32位用来记录读锁的重入次数,这样就可以在一个CAS操作里面呢完成读写操作的检查了,ReadWriteLock的加锁和解锁的原理其实和ReentrantLock大体上差不多,唯一的区别呢就是在检查能否加锁的时候区分读写锁的特性就是读锁之间是共享的,写锁之间、写锁和读锁之间是互斥的

  • 问: 那我们来聊聊 Redis Redis是单线程的吗?

嗯,不是的,Redis应该是在4.0版本之后就不是单线程的已经新增了线程还有后台线程

  • 问:那简单介绍一下这些线程都是用来干什么的吧?

Redis最核心的是主线程,它主要是做三个事情,一个是监听IO多路复用器上发生的这个读写事件,我们拿这个读事件举例吧,在监听到i读事件的时候呢,主线程就会把发生读事件的连接分发给IO线程,然后IO线程会读取和解析请求,然后把解析好的命令,还有这个参数放到缓冲区里面去,主线程要做的这个第二个事情呢就是执行IO线程里面读取好的命令,比如说 IO读取到了一个SET命令放到了缓冲区里面,然后主线程就会从缓冲区里面读取这个命令,然后修改redis内存里面的数据,这个时候命令执行产生的返回值就会写到缓冲区里面,之后在连接发生可写事件的时候,IO线程会把 SET命令返回值发送给客户端,然后我们说最后一个主线程最后要做的呢就是执行定时任务,就有一些定时任务比如说更新时钟、检查和客户端的心跳、和从库的心跳之类的都是在定时任务当中完成的,然后我们再说一下IO线程,IO线程它主要就是干两个事情,一个是从连接里面读取数据,然后反序列化得到 Redis命令,另一个呢就是把命令执行完的返回值序列化成字节数组,然后通过连接返回到了这个客户端,最后一种线程就是后台线程,我记得是有三个后台线程来着,其中我印象比较深的呢就是用来释放内存空间的后台线程,比如说在我们删除一个key的时候要回收会比较耗时嘛个空间的时候会比较耗时嘛,所以在主线程里面进行的话就会阻塞主线程,然后整个redlis的性能就会下来,所以redis就会把这种回收内存空间的操作放到后台线程进行

  • 问:在 Redis里面我存储了一个值Key 1,value是100,现在有两个线程并发的去修改进行加一操作应该怎么实现呢?

嗯使用increment的命令就行

  • 问: 现在redis里边儿有一个值是 Key,它里面是一个空字符串,我现在还是有两个线程就并发去加字符,一个线程追加A:一个线程追加B。我现在需要通过代码控制这个学符串儿是ABAB 这种间隔的方式出现啊,这个我应该怎么实现呢?

我觉得可以加一个分布式锁,这样的话呢就把两个线程锁住了,比如说用SETNX方式,每次去更新这个字符串儿的线程可以先去拿一下这个锁,拿到这个锁之后才去更新这个字符串,没有拿到锁的那个就需要等待或者是sleep

  • 问: 除了分布式锁之外,还有别的方式吗,可以考虑一下 CAS的方式。

CAS的方式就是要保证读取,比较和写入都是原子操作的,这个时候我可以使用lua脚本,在lua本中判断一下这个当前字符串是以什么字符结尾的,然后决定当前线程是否要追加

  • 问: 你刚才提到 Redis的分布锁可以使用SETNX命令实现可以展开介绍一下使用SETNX命令实现分布式锁的思路吗?

好的SETNX命令的含义是当这个key没有的时候就在redis 里面写入这个值,我们可以把key值作为锁的名字,value值就是当前拿锁的服务标识和线程ID组成的这个字符串,我们还可以给这个key加一个过期时间戳这样的话,如果我们的服务或者是线程异常退出了,可以保证我们的锁能够在一段时间之后被自动的释放,这样的话呢

其他线程就能够继续获取这个锁,然后也不会出现死锁,大概就是这么实现的

  • 问: 那描述一下这个方案里面锁释放的过程吧。

在一个线程释放锁的时候呢,会先去 get一下这个锁的value值然后和自己的服务标识还有线程D进行比较,这个时候如果是一致的话就证明当前线程是拿着锁的,这样的话当前线程它就有资格去释放这个锁就可以直接把这个key删掉,或者是把value值设置为空这样也可以

  • 问:那我们来考虑一个并发场景啊﹖如果当前我们拿着锁的线程去释放的时候,检查也通过了这个时候去删除Key之前发生了 GC 一直到GC到这个锁过期了﹔这个时候呢又有另外第二个线程来拿锁,拿成功了,GC了嘛,这个时候呢。我们前一个线程删除锁的那个请求才到,那我们怎么处理这个情况呢

如果是这种场景的话,我们也可以在释放锁的时候,也用lua脚本去实现一个CAS的操作,大致的流程就是,我把当前释放锁的服务标识和线程组成的这个字符串,传给了这个lua脚本,然后这个lua脚本 它里面会进行判断是不是和当前的这个value值是不是一致的,如果一致的话呢它就释放锁,如果不一致的话它就没有办法释放锁

  • 问: 那我们 Redis—般有什么高可用的方案吗

高可用的话,可以有主同复制,我们可以给一个redis的主库加多个从库,然后也可以给它加上这个哨兵的机制,哨兵的话, 一个就是 监控主从集群里面Redis实例是不是有宕机的,然后呢就是在出现主库宕机的时候进行自动的故障转移,让那个从节点提升为主节点啊,然后还会给客户端返回新的主库的地址,这样的话就是客户端就可以直接连接到新的主库上面,除了这个主从,哨兵模式之外呢,我们还可以使用redis集群,redis集群的话呢,它就自带了高可用方案,redis集群的话是所有的集群节点里面,不管是主节点还是从库,他们之间都是有连接的,然后通过这个心跳去进行检查可用性,当一个节点宕机的时候呢,其他节点都是能够感知到的然后它们好像都是通过Gossip协议进行实现,嗯然后当这个超过一半节点的时候就认为这个节点宕机就会对它进行故障转移

  • 问: 你刚才说到Redis主从复制的这个事情,他主从复制是同步的还是异步的?

这个是异步的

  • 问: 如果是这样的话,我在进行主从切换的时候是没办法保证所有的从节点都跟主节点数据是致的对吧那在主库宕机的时候,我们应该选择哪个从节作为新的主库呢?

这个时候呢一般会来比较一下几个从节点的同步状态,主从复制里面呢,有一个 offset偏移量的概念,如果从节点复制的越快那这个offset也就越大也就证明了这个从库和主库之间的数据状态就越接近,这个时候 我们应该选这个offset值最大的从库作为主库

  • 问: 回到分布式锁的那个问题,如果我们的主库右机了。然后我们setnx的那个命令还没有同步到从节点啊这个时候如果从节点提升为主库的时候会有件么问题呢?

这个呢就可能导致分布式锁会失效

  • 问:那我们有什么方案来解决这个问题吗?

我记得redlis有一个命令,可以有一个WAIT命令,这个WAIT命令呢它可以暂时的阻塞客户端,然后保边我们的命令同步到至少一个或者是多个从库上面去

我记得好像是有这个命令来着

  • 问:那线上的话是一主多从那wait命令这个从节点个数应该设置多少个呢?

那就把它设置成这个从库的个数吧,保证它能够同步到全部的从库上面去

  • 问:如果设置全部的话:基中有一个从节点宕机了,那我们的WAIT命令是不是会一直阻塞在那里?

那就设置成1吧只要同步到一个从库就行嘛

  • 问:还有另一种情况,就是这个命令刚同步到这个从节点,主节宕机了然后其他没有同步到数据的从节点变为了主节点这样的话你那个锁依然是失效的对吧

那感觉设置多少都不是很合适,那我可以根据当前集群的节点数,然后设置半数以上的节点,同步到这个命令,然后再wait命令返回

  • 问这个分布式锁还有什么篑他问题嘛?可以再考虑下

嗯好像也没有什么太大的问题了

  • 问:我们举个例子啊,在获取锁服务之后出现了长时间的GC,然后锁超时释放了,这个时候,有其他服务拿到了这个锁,然后我们的服务从GC 里面苏醒了,这个时候就变成两个线程并行执行业务逻辑了,这种属手分布式锁意外丢失的情况。

这个就不太好解决了。

  • 问:有没有了解过RedLock的原理?

哦这个没太了解过

  • 问: 那你还知道哪些分布式锁的实现方式呢?

除了这个redis 之外,我们还可以使用MySQL或者是Zookeeper 来实现分布式锁

  • 问:可以展开说说这两种实现方式的原理吗?

嗯嗯好的,MySQL的话呢就类似于乐观锁的原理,我们在每一行数据里面都会添加一个版本号,我们先用select语句把这个数据查出来,然后进行计算当这个计算完成之后更新的时候呢就会把版本号放到update语句的 where条件里面进行比较,这时候比较一下要是版本号没有发生变化就说明更新成功了,就表示没有其他的线程或者是服务并发更新,如果说是更新失败了就是存在其他线程并发修改过这个记录,那我们就要重新读取这行数据,然后再进行计算,再用新的版本号进行更新,使用MySQL实现分布式锁的话,如果 MySQL是主从复制的模式也会有你说的那个主从延迟的问题,所以读写呢都要在主库上。Zookeeper的话呢,我们可以使用里面的临时节点,Zookeeper的临时节点呢有这样的特性就是在客户端断开连接之后,这个临时节点呢就会被Zookeeper集群自动删除掉,然后Zookeeper的里面的另一个功能呢就是watcher,它可以监听这个Zookeeper 里面一个节点的变化,这样的话呢,其他客户端呢就可以监听到这个临时节点的删除,然后这个时候我们就可以从我们可以以这个临时节点作为这个锁,然后客户端可以及竞争去创建同一个临时节点,哪个客户端创建成功就明它持有锁,当它这个释放锁的时候可以把这个临时节点删除掉,然后其他没有获取到锁的那些服务就可以通过watcher 监听到这个临时节点,当这个持有者的服务把这个临时节点删掉的时候呢,在这个时候其他的服务就可以通过watcher监听到这个节点的删除事件,然后再来竞争这条锁

  • 问:有一个小问题啊·就是zookeeper的这个分布式锁·如果是这个临时节点被删除了。就是锁释放了其他全部监听这个锁的服务都会被唤醒伯是最后竞争到锁的只有一个这样有点浪费资源,没有什么方武只唤醒一个或者几个少量的服务来竞争这把锁呢。

那我们可以使用临时顺序节点,这样的话呢使用目录来表示个锁,然后每个服务呢都会在一个锁目录下面创建一个临时节点,然后去监听比官小一号的那个临时顺序节点,这样的话呢在释放锁的时候,服务它就会删除自己对应的临时顺序节点,然后只有一个服务在监听这个临时节点,这样就会只唤醒这一个服务实例了

  • 问: 简单聊一下Zookeeper吧·我们现在有一个五个节点组成的Zookeeper 集群·你可以简单描述下这个Zookeeper集群在启动的时候都发生了什么吗,可以尽可能的把你知道的一些细节都可以描述一下。

在那个Zookeeper 里面的有一个配置文件,我们在启动之前会先在这个配置文件里面指定这个集群里面其他四个节点的地址,主要是 这四个节点的host还有端口这些信息还会指定这个Zookeeper集群的编号,然后可以启动这个Zookeeper,在一个Zookeeper实例启动之后呢,它会尝试去连接这个配置文件指定的其他四个节点连接完成之后呢就开始进行leader选举,这个时候呢每个服务都会投自己一票,然后把这个投票的信息发布给其他的节点,这样的话呢,集群里面每个节点呢都可以看到其他四个节点投票的结果,比如说 我是编号为1的这个服务节点,这个时候呢 我就可以看到其他四个节点都分别投了自己一票嘛,然后就是2345这种投票结果啊,然后因为每个节点呢都没有拿到超过半数的这个投票我们这一轮投票就失败了,我们就会开始进行第二轮的投票,那第二轮投票的话呢会根据我们第一轮投票信息进行分析,这个投票里边儿带了ZXID,那这个时候整个集群里面所有的数据都是空的,ZXID都是零,那如果ZID一致的话,就会把这个票投给编号最大的那个服务节点,也就是这个集群里面编号为5的那个节点,然后这样的话呢,这轮投票编号为5的这个节点呢就能拿到半数以上的这个票数,这个时候 这个编号为5的这个节点就成为了这个预备leader,然后像1234这些节点就也能收到其他节点的投票结果,也就知道了这个 节点5成为了预备leader,然后成为这个预备leader之后呢,编号为5的这个节点呢就会进行确认然后再向这个编号,1234这四个节点发送这个确认信息让他们成为follower节点

  • 问:刚才你提到有一个ZXID的概念,这个在ID里边儿带了一些件么样的信息呢?这个ZXID是干什么用的呢?

我记得ZXID包括两部分,一个是高位存储的一个叫纪元的概念,我们每进行一次选举呢,就比如说我的leader宕机了,然后我们每进行一次选举我们的纪元就会加一,然后低位存储的呢就是这个纪元里面的ID这个是自增的,这样的话呢就组成了一个ZXID,它是全局唯一的,因为Zookeeper 只能写主节点,那我们的这个写请求就会全部落到leader上,那我们的leader 就会生成一个ZXID用来唯—标识这次写操作,然后这个ZXID和这次写操作都会同步到这个follower节点上,然后超过半数的follower节点收到并且确认了这次写操作,那么这个写操作才会真正的提交,这个ZXID的话就可以唯一标识—次写操作,然后因为它是那个是纪元内顺序递增的,我们就可以保证我们的写操作是有序的

  • 问:你刚才说到那个写只能写leader节点,那我们读操作呢?

读的话呢是可以读follower节点的

  • 问: 有一个读多写少的场景,那读压力非常大,那我有什么方法来提高K集群的性能呢?

那我们可以添加follower节点嘛,因为follower节点的话都能处理读请求,所以说我们增加follower节点的话就可以提高这个读的性能

  • 问: 增加follower节点的话会有什么其他问题吗?整个ZK集群的写操作会不会变慢呢?

这个时候可能是会变慢,因为我的写操作是要同步到半数以上的follower节点当中,增加这个follower节点会降低写操作,我们可以增加observer节点,observer节点呢可以处理读请求,但是它又不会参与选举和写操作的提交,它只是单纯的同步数据,然后处理这个读操作

  • 问:Zk是不能保证强一致性﹔只能保证最终一致性对吧。会不会有这种情况啊就拿前面你说的那个分布式锁的场景来说,有A、B两个服务去竞争把锁A写入的临时顺序节点比较小,加锁成功了,然后这个时候儿呢,B写入的临时顺序节点呢,同步到了follower ,而A的没同步过去,这个时候B服务从follower节点进行读取的时候。就会认为自己加锁成功了,A服务读取的时候呢就没读取到自己的节点,就认为自己没有加锁成功·过一段时间呢,A又同步过来了﹔A节点会认为自己加锁成功了。

应该不会有这个问题,因为如果A创建了这个临时节点比B小的话,那A节点的写请求应该是先到达的,那这样的话,应该会比B节点写的那个临时节点先同步到follower里面去所以不会出现这种情况