Zookeeper非公平锁实现

image.png
这种加锁方式是非公平锁的具体实现。所有的连接都在对同一个节点进行监听,当服务器检测到删除事件时,要通知所有的连接,所有的连接同时收到事件,再次并发竞争,这就是羊群效应

Zookeeper公平锁实现

image.png

  1. 请求进来,直接在/lock节点下创建一个临时顺序节点
  2. 判断自己是不是lock节点下,最小的节点
    1. 是最小的,获得锁
    2. 不是,对前面的节点进行监听(watch)
  3. 获得锁的请求,处理完释放锁,即 delete 节点,然后后继第一个节点将收到通知,重复第2步判断

这种实现方式所有加锁请求都进行排队加锁,是公平锁的具体实现。借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。

Zookeeper共享锁实现

前面这两种加锁方式都是互斥锁,同一时间只能有一个请求占用。如果是大量的并发上来,性能是会急剧下降的,所有的请求都得加锁。那是不是真的所有的请求都需要加锁呢?答案是否定的,比如如果数据没有进行任何修改的话,是不需要加锁的,但是如果读数据的请求还没读完,这个时候来了一个写请求,怎么办呢?有人已经在读数据了,这个时候是不能写数据的,不然数据就不正确了。直到前面读锁全部释放掉以后,写请求才能执行,所以需要给这个读请求加一个读锁标识,让写请求知道,这个时候是不能修改数据的。不然数据就不一致了。如果已经有人在写数据了,再来一个请求写数据,也是不允许的,这样也会导致数据的不一致,所以所有的写请求,都需要加一个写锁,是为了避免同时对共享数据进行写操作。(读读不互斥,读写互斥,写读互斥,读读互斥
Zookeeper典型使用场景 - 图3

注册中心

Zookeeper典型使用场景 - 图4
服务不再是A-B或B-C那么简单,而是错综复杂的微小服务的调用。这个时候我们可以借助于Zookeeper的基本特性来实现一个注册中心。什么是注册中心?顾名思义,就是让众多的服务,都在Zookeeper中进行注册。什么是注册?注册就是把自己的一些服务信息,比如IP,端口等一些更加具体的服务信息都写到 Zookeeper节点上, 这样有需要的服务就可以直接从zookeeper上面去拿。怎么拿呢?这时我们可以定义统一的名称,比如User-Service, 那所有的用户服务启动的时候,都在User-Service 这个节点下面创建一个子节点(临时节点),这个子节点保持唯一就好,代表了每个服务实例的唯一标识,有依赖用户服务的比如Order-Service,就可以通过User-Service这个父节点,就能获取所有的User-Service 子节点,并且获取所有的子节点信息(IP,端口等信息),拿到子节点的数据后Order-Service可以对其进行缓存,然后实现一个客户端的负载均衡。同时还可以对这个User-Service 目录进行监听, 这样有新的节点加入或者退出,Order-Service都能收到通知,Order-Service重新获取所有子节点且进行数据更新。这个用户服务的子节点的类型为临时节点。 Zookeeper中临时节点生命周期是和SESSION绑定的,如果SESSION超时了,对应的节点会被删除,被删除时,ZK会通知对该节点父节点进行监听的客户端,对应的客户端又可以刷新本地缓存了。当有新服务加入时,同样也会通知对应的客户端,刷新本地缓存,要达到这个目标需要客户端重复的注册对父节点的监听。这样就实现了服务的自动注册和自动退出。
Zookeeper典型使用场景 - 图5
Spring Cloud生态Spring Cloud Zookeeper项目提供了Zookeeper注册中心的实现

以两个服务 user-center : 用户服务和 product-center: 产品服务为例。
用户调用产品服务,且实现客户端的负载均衡,产品服务自动加入集群,自动退出服务。

user-center 服务配置

  1. spring.application.name=user-center
  2. #zookeeper 连接地址
  3. #如果使用了spring cloud zookeeper config这个配置
  4. ##置在 bootstrap.yml/bootstrap.properties
  5. spring.cloud.zookeeper.connect-string=192.168.109.200:2181
  6. #将本服务注册到zookeeper,如果不希望自己被发现可以配置为false, 默认为 true
  7. spring.cloud.zookeeper.discovery.register=true
  1. @SpringBootApplication
  2. public class UserCenterApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(UserCenterApplication.class, args);
  5. }
  6. @Bean
  7. @LoadBalanced
  8. public RestTemplate restTemplate(){
  9. return new RestTemplate();
  10. }
  11. }
  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.web.bind.annotation.GetMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. import org.springframework.web.client.RestTemplate;
  5. @RestController
  6. public class TestController {
  7. @Autowired
  8. private RestTemplate restTemplate;
  9. @GetMapping("/test")
  10. public String test(){
  11. return this.restTemplate.getForObject( "http://product-center/getInfo",
  12. String.class);
  13. }
  14. }

product-center服务配置

  1. spring.application.name=user-center
  2. #zookeeper 连接地址
  3. spring.cloud.zookeeper.connect-string=192.168.109.200:2181
  4. #将本服务注册到zookeeper
  5. spring.cloud.zookeeper.discovery.register=true
  1. @SpringBootApplication
  2. @RestController
  3. public class ProductCenterApplication {
  4. @Value("${server.port}")
  5. private String port;
  6. @Value( "${spring.application.name}" )
  7. private String name;
  8. @GetMapping("/getInfo")
  9. public String getServerPortAndName(){
  10. return this.name +" : "+ this.port;
  11. }
  12. public static void main(String[] args) {
  13. SpringApplication.run(ProductCenterApplication.class, args);
  14. }
  15. }

问题一:redis锁和zookeeper锁有什么区别?具体选哪个呢?

可靠性:zookeeper的比redis更强一点。因为redis是master slave结构,如果分布式锁,写完master后,还没及时同步到slave时,这次分布式锁是无效的。但zookeeper是leader follower过半才能写成功。
性能方面:redis性能好一点,zookeeper性能差一些。zookeeper锁写的效率比较差,redis的写的效率比较高。

问题二:客户端向MySQL发送一个请求的大概流程是什么样的?

当向 MySQL 发送一个请求的时候,会发生以下事情:
1)客户端发送一条查询给服务器。
2)服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段(当然从 MySQL8.0 开始,这个部分就没有了)。
3)服务器端进行 SQL 解析、预处理,再由优化器生成对应的执行计划。
4) MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询。
5)将结果返回给客户端。
所以 MySQL 查询的生命周期大致可以按照顺序来看:从客户端到服务器,然后在服务器上进行解析,生成执行计划,执行,并返回结果给客户端。所以查询由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数, 要么让子任务运行得更快。其中“执行”可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。但是决定不能认为其他子任务对查询的性能毫无影响。

问题三:Zookeeper可以在哪些场景使用?

集群管理:监控节点存活状态、运行请求等;
主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 Zookeeper 可以协助完成这个过程;
分布式锁:Zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。Zookeeper 可以对分布式锁进行控制。
命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。

问题四:什么是InnoDB的Buffer Pool?

在WAL机制中,InnoDB内存的一个作用是保存更新的结果,再配合redo log,避免了随机写盘。内存的数据页是在Buffer Pool中管理的,在WAL里Buffer Pool起到了加速更新的作用
由于有WAL机制,当事务提交的时候,磁盘上的数据页是旧的,如果这时候马上有一个查询要来读这个数据页,不需要马上把redo log应用到数据页。因为内存数据页的结果是最新的,直接读内存就可以了。Buffer Pool起到了加速查询的作用
Buffer Pool对查询的加速效果依赖于内存命中率。可以在show engine innodb status结果中,查看一个系统当前的BP命中率
InnoDB Buffer Pool的大小是由参数innodb_buffer_pool_size确定的,一般建议设置成可用物理内存的60%~80%