分布式专题

分布式专题

这里将持续拓展增加一些分布式相关的核心要点。

  • 消息中间件

    • Kafka

    • RabbitMQ

  • 微服务

    • Dubbo

    • Spring Boot

    • Spring Cloud

  • Zookeeper

  • Hadoop

  • Dobbo

1. 分布式锁

金山云面经

  • 为什么要使用分布式锁?

    • 在单体应用单机环境下,ReentrantLock 或 Synchronized 进行互斥控制。在单机环境中,Java 中提供了很多并发处理相关的 API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 Java API 并不能提供分布式锁的能力。为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

  • 分布式锁一般有三种实现方式:

    • 数据库乐观锁;

    • 基于 Redis 的分布式锁;

    • 基于 Zookeeper 的分布式锁。

    尽管有这三种方案,但是不同的业务也要根据自己的情况进行选型,他们之间没有最好只有更适合!

数据库实现

基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

(1)创建表

要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。

当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。

UNIQUE 约束唯一标识数据库表中的每条记录,每个表可以有多个 UNIQUE 约束.

(2)想要执行某个方法,就使用这个方法名向表中插入数据:

因为我们对 method_name 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

(3)成功插入则获取锁,执行完成后删除对应的行数据释放锁:

上面这种简单的实现有以下几个问题:

  1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

  2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

  3. 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

  4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

以上问题的解决方案:

  1. 数据库是单点?

    搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。

  2. 没有失效时间?

    只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

  3. 非阻塞的?

    搞一个 while 循环,直到 insert 成功再返回成功。

  4. 非重入的?

    在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

Redis实现

1、选用Redis实现分布式锁原因:

  1. Redis 有很高的性能;

  2. Redis 命令对此支持较好,实现起来比较方便

2、使用命令介绍:

在使用 Redis 实现分布式锁的时候,主要就会使用到这三个命令。

  1. SETNX

  1. expire

  1. delete

3、实现思想:

  1. 获取锁的时候,使用 setnx 加锁,并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的 value 值为一个随机生成的 UUID,通过此在释放锁的时候进行判断。

  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

  3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

注解:UUID是128位长的数字,一般用16进制表示,结合机器的网卡、当地时间、一个随机数来生成UUID

Zookeeper实现

ZooKeeper 是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

基本思路:每个客户端对某个方法加锁时,在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

Zookeeper能不能解决前面提到的问题。

  • 锁无法释放?使用 Zookeeper 可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在 ZK 中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session 连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

  • 非阻塞锁?使用 Zookeeper 可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper 会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。

  • 不可重入?使用 Zookeeper 也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。

  • 单点问题?使用 Zookeeper 可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

三种方案的比较

  • 从实现的复杂性角度(从低到高) Zookeeper >= 缓存 > 数据库

  • 从性能角度(从高到低) 缓存 > Zookeeper >= 数据库

  • 从可靠性角度(从高到低) Zookeeper > 缓存 > 数据库

参考资料

最后更新于