目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

Zookeeper

注册中心

 微服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器,维护带来很大问题。常见的注册中心:zookeeper、Eureka、consul、etcd。
注册中心

  • 理解注册中心:服务管理,核心是有个服务注册表,心跳机制动态维护。
  • 服务提供者 provider: 启动的时候向注册中心上报自己的网络信息。
  • 服务消费者 consumer: 启动的时候向注册中心上报自己的网络信息,拉取 provider 的相关网络信息。

安装 JDK1.8

# 解压
tar -zxvf jdk-8u191-linux-x64.tar.gz

# 移动到/usr/local目录下
 mv jdk1.8.0_191 /usr/local/

# 修改/etc/profile,在文件的最后面加上下面几行
vim /etc/profile
JAVA_HOME=/usr/local/jdk1.8.0_191
JRE_HOME=$JAVA_HOME/jre
PATH=$JAVA_HOME/bin:$PATH
export PATH

# 激活配置
source /etc/profile

# 验证
[root@localhost test]# java -version
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

安装 Zookeeper

# 解压
tar -zxvf zookeeper-3.4.12.tar.gz 
mv zookeeper-3.4.12 /usr/local/

# 修改配置文件
cp /usr/local/zookeeper-3.4.12/conf/zoo_sample.cfg  /usr/local/zookeeper-3.4.12/conf/zoo.cfg
vim /usr/local/zookeeper-3.4.12/conf/zoo.cfg 
	dataDir=/usr/local/zookeeper-3.4.12/data

# 创建数据目录
mkdir /usr/local/zookeeper-3.4.12/data
useradd zookeeper
chown -R zookeeper:zookeeper /usr/local/zookeeper-3.4.12/

# 启动服务
bash /usr/local/zookeeper-3.4.12/bin/zkServer.sh  start

# 使用zkCli连接Zookeeper
.\zkCli.cmd -timeout 5000 -server 192.168.31.220:2181
Connecting to 192.168.31.220:2181
ientCnxn$SendThread@1302] - Session establishment complete on server 192.168.31.220/192.168.31.220:2181, sessionid = 0x10014aa999f0000, negotiated timeout = 5000
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
[zk: 192.168.31.220:2181(CONNECTED) 0] stat /
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

#Zookeeper 的数据模型
数据模型

  • zookeeper 的数据模型类似于 Linux 下的文件目录。
  • 每一个节点都叫做 zNode,可以有子节点,也可以有数据
  • 每个节点都能设置相应的权限控制用户的访问(ACL)。
  • 每个节点存储的数据不宜过大。
  • 每个节点都带有一个版本号,数据变更时,版本号变更(乐观锁)
  • 节点分永久节点跟临时节点(session 断开,临时节点便会自动删除)

zkCli

# 创建顺序节点soulboy 数据是soulboy_data
[zk: 192.168.31.220:2181(CONNECTED) 1] create -s /soulboy soulboy_data
Created /soulboy0000000000

# 创建临时节点
[zk: 192.168.31.220:2181(CONNECTED) 2] create -e /zbk zbk_data
Created /zbk

# 获取节点的子节点
[zk: 192.168.31.220:2181(CONNECTED) 4] ls /
[zbk, zookeeper, soulboy0000000000]

# 获取节点的数据
[zk: 192.168.31.220:2181(CONNECTED) 13] get /soulboy0000000000
soulboy_data
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x2
mtime = Tue Jul 23 12:07:49 CST 2019
pZxid = 0x2
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 12
numChildren = 0

# 查看节点的状态
[zk: 192.168.31.220:2181(CONNECTED) 12] stat /soulboy0000000000
cZxid = 0x2 # 事务id(创建新节点)
ctime = Tue Jul 23 12:07:49 CST 2019 #节点创建时间
mZxid = 0x2 # 最后一次更新时,事务的id	(对节点进行修改)
mtime = Tue Jul 23 12:07:49 CST 2019 # 最后一次更新的时间
pZxid = 0x2 # 该节点的子节点列表最后一次被修改的事务id
cversion = 0 # 子节点列表的版本
dataVersion = 0 # 数据内容的版本
aclVersion = 0 # ACL版本
ephemeralOwner = 0x0 # 用于临时节点,表示创建该临时节点的事务id,如果不是临时节点,该字段为0
dataLength = 12 # 数据内容长度
numChildren = 0 # 子节点的数量

# 创建子节点
[zk: 192.168.31.220:2181(CONNECTED) 4] create -e  /soulboy0000000000/test test
Created /soulboy0000000000/test

# 获取节点的子节点以及当前节点的状态
[zk: 192.168.31.220:2181(CONNECTED) 5] ls2 /soulboy0000000000
[test]	# 子节点在此,下面是当前节点的状态
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x2
mtime = Tue Jul 23 12:07:49 CST 2019
pZxid = 0x6
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 12
numChildren = 1

# 修改节点数据
[zk: 192.168.31.220:2181(CONNECTED) 7] set /soulboy0000000000 soulboy_data_new
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x7
mtime = Tue Jul 23 12:37:50 CST 2019
pZxid = 0x6
cversion = 1
dataVersion = 1 #原本是0,修改之后就会加1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 16
numChildren = 1

# 删除节点(无法删除有子节点的节点)
[zk: 192.168.31.220:2181(CONNECTED) 8] delete /soulboy0000000000
Node not empty: /soulboy0000000000

# 递归删除整个节点(可以递归删除有子节点的节点)
[zk: 192.168.31.220:2181(CONNECTED) 8] rmr /soulboy0000000000

Zookeeper Session 机制

Session机制
 临时节点的生命周期跟随 session 的生命周期。当 session 过期的时候,该 session 创建的所有临时节点都会被删除。

# 创建临时节点
create -e /test test

# 关闭session
close

# 登录查看刚才创建的临时节点 :发现test节点已经被删除
.\zkCli.cmd -timeout 5000 -server 192.168.31.220:2181
[zk: 192.168.31.220:2181(CONNECTED) 0] ls /
[zookeeper, soulboy0000000000]

Zookeeper Watcher 机制

Watcher机制
对节点的 watcher 操作 get stat
 针对每一个节点的操作,都可以有一个监控者,当节点数据发生变化,会触发 watcher 事件 zk 中 watcher 是一次性的,触发后立即销毁 所有有监控者的节点的变更操作都能触发 watcher 事件。
子节点的 watcher 操作 ls ls2
 监控父节点,当父节点对应的子节点发生变更的时候,父节点上的 watcher 事件会被触发, 增删会触发、修改不会数据则不会触发,如果子节点再去新增子节点,也不会触发(也就是说,触发 watcher 事件一定是直系子节点)。

# 监控当前节点的数据变化:get、stat
[zk: 192.168.31.220:2181(CONNECTED) 3] get /soulboy0000000000 watch

#  监控当前的直系子节点们(增删子节点):ls、ls2
[zk: 192.168.31.220:2181(CONNECTED) 3] ls /soulboy0000000000 watch

Zookeeper ACL 权限控制

 针对节点可以设置相关的读写等权限,目的是为了保证数据的安全性,权限 permissions 可以指定不同的权限范围及角色,只有拥有相应权限和角色的用户才可以访问对应的 ZNODE。

  • 开发环境跟测试环境,使用 acl 就可以进行分离,开发者无权去操作测试的节点。
  • 生产环境上控制指定 ip 的服务可以访问相关的节点。
# scheme授权机制
* acl组成[scheme:id:permissions]
* world下只有一个id,也就是anyone,表示所有人 
* world:anyone:permissions
* auth:代表认证登录,需要注册用户有权限才可以 
* auth:user:password:permissions
* digest 需要密码加密才能访问 
* digest:username:BASE64(SHA1(password)):permissions(跟auth区别在于,auth明文,digest为密文)
* ip:ip:localhost:psermissions
* super:代表超管,拥有所有的权限;
* id:允许访问的用户
* permissions:权限组合字符串
	cdrwa
	c create 创建子节点
	d delete 删除子节点
	r read 读取节点的数据
	w write 写入数据
	a admin 可管理的权限
	cdrwa cdr cdw 

# 获取节点权限 getAcl
[zk: 192.168.31.220:2181(CONNECTED) 4] getAcl /soulboy0000000000
'world,'anyone
: cdrwa

#设置节点权限 setAcl 
[zk: 192.168.31.220:2181(CONNECTED) 6] addauth digest soulboy:soulboy   # 添加用户
[zk: 192.168.31.220:2181(CONNECTED) 7] setAcl /soulboy0000000000 auth:soulboy:soulboy:cdrwa
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x7
mtime = Tue Jul 23 12:37:50 CST 2019
pZxid = 0x9
cversion = 2
dataVersion = 1
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 16
numChildren = 0

# 查看权限soulboy0000000000节点权限
[zk: 192.168.31.220:2181(CONNECTED) 8] getAcl /soulboy0000000000
'digest,'soulboy:VYanT576Pu1fFRBjBed+WuMZJJI=
: cdrwa

# 使用zkCli新建终端来验证
[zk: 192.168.31.220:2181(CONNECTED) 1] get /soulboy0000000000
Authentication is not valid : /soulboy0000000000

# 登录指定用户,再次查看soulboy0000000000节点
[zk: 192.168.31.220:2181(CONNECTED) 2] addauth digest soulboy:soulboy
[zk: 192.168.31.220:2181(CONNECTED) 3] get /soulboy0000000000
soulboy_data_new
cZxid = 0x2
ctime = Tue Jul 23 12:07:49 CST 2019
mZxid = 0x7
mtime = Tue Jul 23 12:37:50 CST 2019
pZxid = 0x9
cversion = 2
dataVersion = 1
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 16
numChildren = 0

# 修改权限:少了d,代表没有删除子节点的权限
[zk: 192.168.31.220:2181(CONNECTED) 4] setAcl /soulboy0000000000 auth:soulboy:soulboy:crwa

Zookeeper Cluster Role

Zookeeper Cluster Role

  • leader:作为整个 zk 集群写请求的唯一处理者,并负责进行投票的发起和决议(集群中过半节点同意提议,才可以写入),更新系统的状态。
  • follower:接收客户端请求,处理读请求,并向客户端返回结果;将写请求转给 Leader;在选举 Leader 过程中参与投票。
  • observer:可以理解为无选举投票权的 Flollower,其主要是为了协助 Follower 处理更多的读请求。如果 Zookeeper 集群的读请求负载很高,或者客户端非常非常多,多到跨机房,则可以设置一些 Observer 服务器,以提高读取的吞吐量。ZK Server 节点宕机过半,则整个 Zookeeper Cluster 不可用,而 observer 宕机不会影响 Zookeeper Cluster。

Zookeeper Cluster Mode

 Zookeeper Cluster 的核心是广播机制,该机制保证了各个 zk 之间数据同步(数据一致性)。zk 实现的机制为 ZAB 协议。

  • 恢复模式: 如果 leader 崩溃,这个时候就会进入恢复模式,使整个 zk 集群恢复到正常的工作状态。(进行选举新 Leader 等一系列操作)
  • 同步模式:新的 leader 选举出来后,就乎进入同步模式(各个 follower 会去同步新的 leader 上的数据),当大多数 zkServer 完成了与 leader 的状态同步之后,恢复模式就结束。
  • 广播模式:客户端想写入数据,这个时候 leader 发起提议,当 leader 的提议被大多数的 zkServer 统一之后,leader 就会去修改自身的数据,并将修改后的数据广播给其他的 follower。

Zookeeper Cluster Mode of Election

核心概念

# myid
这是Zookeeper Cluster中服务器的唯一标识,称为 myid。例如,有三个 zk 服务器,那么编号分别是 1,2,3。

# zxid
ReentranReadWriteLock 32位
高位              低位
0000000000000000  0000000000000000
                    epoch                                 xid
00000000000000000000000000000000   00000000000000000000000000000000
zxid 为 Long 类型,其中高 32 位表示 epoch,低 32 位表示 xid。即 zxid 由两部分构成:epoch 与 xid。 
每个 Leader 都会具有一个不同的 epoch 值,表示一个时期、时代。新的 Leader 产生,则会更新所有 zkServer 的 zxid 中的 epoch。 而 xid 则为 zk 的事务 id,每一个写操作都是一个事务,都会有一个 xid。每一个写操作都需要由 Leader 发起一个提议,由所有 Follower 表决是否同意本次写操作。

# 逻辑时钟
逻辑时钟,Logicalclock,是一个整型数,该概念在选举时称为 logicalclock,而在 zxid 中则为 epoch 的值。即 epoch 与 logicalclock 是同一个值,在不同情况下的不同名称。

Zookeeper Cluster 选举状态

  • LOOKING,选举状态(查找 Leader 的状态)。
  • LEADING,领导者状态。处于该状态的服务器称为 Leader。
  • FOLLOWING,随从状态,同步 leader 状态。处于该状态的服务器称为 Follower。
  • OBSERVING,观察状态,同步 leader 状态。处于该状态的服务器称为 Observer。

Zookeeper Cluster Electioneering

发生时机
 整个集群群龙无首的时候

  • Zookeeper Cluster 服务首次启动。
  • Zookeeper Cluster 运行中,Leader 宕机的时候。

选举机制
 集群中,半数 zkServer 同意,则产生新的 leader(搭建集群时,一般都是奇数个),三台服务器,最多允许一台宕机,四台服务器,也是最多允许一台宕机。
选举算法
 对比(myid,zxid),先对比 zxid,zxid 大者(大表示数据越新)胜出,成为 leader,如果 zxid 一致,则 myid 大者成为 leader。

Zookeeper Cluster 部署

 端口的作用

  • 2181:对 client 端提供服务
  • 2888:集群内及其通讯使用的端口
  • 3888:集群选举 leader
# 修改/etc/hosts文件
192.168.31.220 zkcluster1
192.168.31.240 zkcluster2
192.168.31.250 zkcluster3

# 添加zookeeper用户
useradd zookeeper

# 修改zk配置文件zoo.cfg
dataDir=/usr/local/zookeeper-3.4.12/data	#数据文件存放目录
server.1=zkcluster1:2888:3888	#分别添加集群中的每台ZK Server节点。
server.2=zkcluster2:2888:3888
server.3=zkcluster3:2888:3888	

# 在/usr/local/zookeeper-3.4.12/data目录下新建myid的文件
cat /usr/local/zookeeper-3.4.12/data myid	# zkcluster1
1
cat /usr/local/zookeeper-3.4.12/data myid	# zkcluster2
2
cat /usr/local/zookeeper-3.4.12/data myid	# zkcluster3
3

# 赋权zookeeper用户
chown -R zookeeper:zookeeper /usr/local/zookeeper-3.4.12/

# 关闭防火墙
systemctl stop firewalld.service

# 启动服务
bash /usr/local/zookeeper-3.4.12/bin/zkServer.sh  start

# 删除所有windows风格的启动脚本
rm -rf /usr/local/zookeeper-3.4.12/bin/*.cmd

# 为启动脚本增加执行权限
chmod +x /usr/local/zookeeper-3.4.12/bin/*.sh

# 查看状态
zkServer.sh status

# 查看进程PID
jsp

分布式锁作用及其原理

为什么要有分布式锁
 分布式服务中,如果各个服务节点需要去竞争资源,无法使用单机多线程中 JDK 自带的锁来解决此类问题,故此时需要分布式锁来协调。

企业中有哪些常见的手段来实现分布式锁
 zookeeper、Redis、memcache

分布式锁的原理
zookeeper
 去创建相应的节点,创建成功,则表示获取到相应的锁,创建失败,则表示获取锁失败,释放锁的时候删除节点。
Redis、memcache
 对应的去设置一个值做为锁的一标志,每次获取锁的时候,判断对应的值是否存在,存在则无法获取,不存在,则设置相应的值,表示获取到锁。(Redis 使用 setnx,memcache 使用 add)

Zookeeper 实现分布式锁的多种方式

 创建节点的时候,一定要创建临时节点,避免应用获取到锁后,宕机,导致锁一致被持有。

  • 程序连接上 ZK 之后,尝试去创建节点,如果创建成功则成果获取锁,如果节点创建失败,程序进行短暂的休眠,之后重试。缺点:不断重试,会消耗资源。
  • 使用 ZK 的 watcher 机制。缺点:引发同一时刻同时唤醒大批等待程序,引起羊群效应。
  • 创建有序的节点。目标节点:/a0 排队进程阻塞在有序节点 /a1 /a2 /an,这样/a0 释放锁之后,每次只需要唤醒一个等待的进程即可,例如:唤醒/a1

基于 Zookeeper 原生的 API 实现分布式锁

  • 原生 API 不支持递归创建节点
  • 如果是单一应用,尽量不要使用分布式锁

引入 Maven 依赖

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.13</version>
</dependency>

基于 zk 实现分布式锁

import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import static org.apache.zookeeper.CreateMode.EPHEMERAL;
import static org.apache.zookeeper.ZooDefs.Ids.OPEN_ACL_UNSAFE;

/**
 * 基于zk实现分布式锁
 */
public class ZkLock {

    private ZooKeeper zooKeeper;

    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    private ZkLock() {
        try {
            zooKeeper = new ZooKeeper("192.1.31.220:2181,192.1.31.240:2181,192.1.31.250:2181", 5000, new ZkWatcher());
            System.out.println(zooKeeper.getState());
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("与zk建立连接=====>"+zooKeeper.getState());

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static ZkLock getInstance() {
        return Singleton.getInstance();
    }

    private class ZkWatcher implements Watcher {
        @Override
        public void process(WatchedEvent event) {
            System.out.println("接收到监听事件=====》"+event);
            if (Event.KeeperState.SyncConnected == event.getState()) {
                countDownLatch.countDown();
            }
        }
    }

    public void lock(Integer id) {
        String path = "/soulboy-product-lock-" + id;
        //创建临时节点,如果创建成功的话,就表示获取锁,如果失败,则不断尝试
        try {
            zooKeeper.create(path,"".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            System.out.println("成功获取锁");
        } catch (Exception e) {
            while (true) {
                try {
                    Thread.sleep(500L);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                try {
                    zooKeeper.create(path,"".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
                } catch (Exception e1) {
                    continue;
                }
                break;
            }
        }
    }

    /**
     * 释放锁,直接删除zk节点
     * @param id
     */
    public void unLock(Integer id) {
        String path = "/soulboy-product-lock-" + id;
        try {
            zooKeeper.delete(path,-1);
            System.out.println("成功释放锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    private static class Singleton {

        private static ZkLock instance;
        static {
            instance = new ZkLock();
        }

        private static ZkLock getInstance() {
            return instance;
        }
    }
}

测试代码

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程安全的操作示例
 */
public class UnSafeThread {

    private static int num = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(10);
    private static ZkLock lock = ZkLock.getInstance();

    /**y
     * 每次调用对num进行++操作
     */
    public static void inCreate() {
        lock.lock(1);
        Thread th=Thread.currentThread();
        System.out.println("当前num数值:" + num + ", Tread name:"+th.getName());
        num++;
        lock.unLock(1);
    }

    public static void test() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    inCreate();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //每个线程执行完成之后,调用countdownLatch
                countDownLatch.countDown();
            }).start();
        }

        while (true) {
            if (countDownLatch.getCount() == 0) {
                System.out.println(num);
                break;
            }
        }
    }
}

控制台输出

CONNECTING
接收到监听事件=====》WatchedEvent state:SyncConnected type:None path:null
与zk建立连接=====>CONNECTED
成功获取锁
当前num数值:0, Tread name:Thread-3
成功释放锁
成功获取锁
当前num数值:1, Tread name:Thread-3
成功释放锁
成功获取锁
当前num数值:2, Tread name:Thread-3
成功释放锁
...
成功获取锁
当前num数值:996, Tread name:Thread-7
成功释放锁
成功获取锁
当前num数值:997, Tread name:Thread-7
成功释放锁
成功获取锁
当前num数值:998, Tread name:Thread-7
成功释放锁
成功获取锁
当前num数值:999, Tread name:Thread-7
成功释放锁
1000

使用 Zookeeper 作为注册中心的问题

核心思想

  • 在实践中,注册中心不能因为自身的任何原因破坏服务之间本身的可连通性
  • 注册中心需要AP,而 Zookeeper 是 CP

使用 Zookeeper 作为服务注册中心

启动Docker本地容器

//获取镜像
docker pull zookeeper:3.5

//运行 Zookeeper 镜像
docker run --name zookeeper -p 2181:2181 -d zookeeper:3.5

Provider 端几乎和使用 Euraka 没有区别
依赖

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
		</dependency>

bootstrap.properties

spring.application.name=waiter-service

application.properties

spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

info.app.author=DigitalSonic
info.app.encoding=@project.build.sourceEncoding@

server.port=0

spring.cloud.zookeeper.connect-string=localhost:2181

Customer 端几乎和使用 Euraka 没有区别
依赖

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
		</dependency>

bootstrap.properties

spring.application.name=customer-service

application.properties

server.port=0

spring.cloud.zookeeper.connect-string=localhost:2181

作者:Soulboy