一 背景:

   随着业务系统的变大出现了服务拆分,服务之间需要完成通信调用。

二 简单案例:

   新建两个springboot项目,分别用来模拟库存服务与订单服务。库存服务提供扣减库存接口:

  


@RestController
@RequestMapping("/stock")
public class StockController {

@PutMapping("/decreStock/{productId}")
public String decreStock(@PathVariable("productId") String productId){
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
String format = simpleDateFormat.format(new Date());
System.out.println("["+format+"]扣减库存:"+productId);
return "success";
}
}
 

订单服务提供一个下单操作,且去调用库存服务扣减库存:

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    RestTemplateBuilder restTemplateBuilder;

    /**
     * 下单服务
     */

    @PostMapping("/new/{productId}")
    public String sale(@PathVariable("productId") String productId){
        RestTemplate build = restTemplateBuilder.build();
        String reqestUrl="http://localhost:8081/stock/decreStock/{productId}";
        build.put(reqestUrl,null,productId);
        return "success";
    }
}

然后对定单服务进行测试:

 

 

  同时也可以看到后台打印信息:

 

 

   通过RestTemplate可以实现服务间通信,但存在的问题是库存服务的调用被硬编码到了订单服务中。这是不利于后期的维护与扩展。同时也存在诸如多服务如何负载均衡,单个服务宕掉之后,如果进行后续的响应处理等问题。由此引入了注册中心的概念。

三 注册中心

    注册中心我个人认为主要是去解决各种服务信息的存储、维护与监控作用。

    zookeeper的演变:

    最开始是谷歌的GFS(分布式文件系统)为了解决分布式一致性问题,实现了一个chubby的框架。分布式一致性问题:我们把请求分为读请求与写请求。假设现在数据中心是集群部署的,存在多个节点。当有多个请求过来的时候,假设每个节点在负载均衡算法下都能分到一些请求,而这些请求会去修改同一个数据,因为是同一份数据存在多个节点中,那么这些节点肯定是要在某个时间点进行数据同步的。假设每个节点都可以进行数据修改,那么最新的数据几乎会被均匀的打散到每个节点(这里就假设是轮询算法),那么每个节点之间要相互同步,并且只有最后一个完成同步的数据的节点能保证其数据是最新的,这样的话过程显然非常复杂。然后就有这样的一种设计思想,通过在选举机制,就是每个节点给集群中的节点投一票去推选出一个主节点,由主节点去复杂写请求,然后其他节点作为从节点只处理读请求,并且主节点通过广播的方式与其他从节点定时同步数据。这样的做法保证了只有一个节点数据是最新的,数据的同步变得简单了。(一致性算法)。

     上述算法的复杂性是在于节点通信存在网络抖动,导致数据丢失,出现不可靠。所以chubby并没有直接使用这种方式,而是提供一个服务,不同的节点往此服务上写同一个数据,相同的数据只能被一个节点写成功,一旦有节点创建成功,那么那个节点就会是主节点(master)。zookeeper即在此基础上实现。

四 分布式锁

    当有多个请求的时候,每个请求都会去zookeeper上同一个指定位置创建一个临时有序节点,因为临时节点可以保证有序性和原子性,那么第一个节点肯定只有一个线程能创建成功,当线程发现自己创建的节点在所有节点中是序号排第一那么就直接获取到了锁,而发现不是第一的就会去监听自己的上一个节点是否已被删除,如果没被删除,当前线程就被阻塞,否则重新尝试获取锁。当有节点被删除,此动作会被监听到会唤醒所有的线程,他们在又会重新去尝试获取锁。

public class MyLock {
    // zk的连接串
    String IP = "10.168.1.212:2181";
    // 计数器对象
    CountDownLatch countDownLatch = new CountDownLatch(1);
    //ZooKeeper配置信息
    ZooKeeper zooKeeper;
    private static final String LOCK_ROOT_PATH = "/Locks";
    private static final String LOCK_NODE_NAME = "Lock_";
    private String lockPath;

    //监视器对象,监视上一个节点是否被删除
    Watcher watcher = new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == Event.EventType.NodeDeleted) {
                //唤醒watcher对象等待池中的线程。listener线程收到zk服务端的通知,去唤醒连接线程(当前线程)。
                synchronized (this) {
                    notifyAll();
                }
            }
        }
    };

    // 打开zookeeper连接
    public MyLock() {
        try {
            zooKeeper = new ZooKeeper(IP, 5000, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getType() == Event.EventType.None) {
                        if (event.getState() == Event.KeeperState.SyncConnected) {
                            System.out.println("连接成功!");
                            countDownLatch.countDown();
                        }
                    }
                }
            });
            countDownLatch.await();
        } catch (Exception ex) {
            ex.printStackTrace();
        }


    }


    /**
     * 获取锁的代码
     * @throws Exception
     */
    public void acquireLock() throws Exception {
        //创建锁节点
        createLock();

        //尝试获取锁
        attemptLock();


    }


    //创建锁节点
    private void createLock() throws Exception {
        //判断Locks是否存在,不存在创建
        Stat stat = zooKeeper.exists(LOCK_ROOT_PATH, false);
        if (stat == null) {
            zooKeeper.create(LOCK_ROOT_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        // 创建临时有序节点
        lockPath = zooKeeper.create(LOCK_ROOT_PATH + "/" + LOCK_NODE_NAME, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println("节点创建成功:" + lockPath);
    }



    //尝试获取锁
    private void attemptLock() throws Exception {
        // 获取Locks节点下的所有子节点
        List<String> list = zooKeeper.getChildren(LOCK_ROOT_PATH, false);
         // 对子节点进行排序
        Collections.sort(list);
         // /Locks/Lock_000000001
        int index = list.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));
        if (index == 0) {
            System.out.println("获取锁成功!");
            return;
        } else {
            // 上一个节点的路径
            String path = list.get(index - 1);
            Stat stat = zooKeeper.exists(LOCK_ROOT_PATH + "/" + path,  watcher);
            if (stat == null) {
                attemptLock();
            } else {
                synchronized (watcher) {
                    //再没获取锁成功之前阻塞当前线程
                    watcher.wait();
                }
                attemptLock();
            }
        }
    }

         //释放锁
    public void releaseLock() throws Exception {
         //删除临时有序节点
        zooKeeper.delete(this.lockPath, -1);
        zooKeeper.close();
        System.out.println("锁已经释放:" + this.lockPath);
    }

    public static void main(String[] args) {
        try {
            MyLock myLock = new MyLock();
            myLock.createLock();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

zookeeper用两点老保障了分布式锁。第一 节点名字是唯一的,即相同目录下决对不会存在两个同名的节点。 第二 节点根据先后创建会有一个有序性。假如你节点名称是随机生成的那就很难找到一个判断标准。

 五 角色

     zookeeper角色分为leader、observer、follower。zookoopeeper集群加启动的时候就会进行投票,选出leader,其他节点就变成了follower,observer不参与选举投票。leader是负责事务操作(写请求),并且定时将数据同步到follower节点。leader宕机之后,也会进行重新选举。

    如果是读请求,可以直接交给非leader节点处理,如果是写请求,只能交给leader处理。并且leader会向所有的follower发起投票,如果有一半以上的follower投票通过,leader直接返回给client端,请求结果,此次写请求成功。这样存在的问题是:在考虑系统的申缩性的时候,我们增加了更多的zk节点,而leader只能有一个,那么leader每次发起投票与收集结果的工作量就会不断变大。为了解决这个问题,就需要用到observer。observer不参与投票,但是作为数据副本,可以帮助处理读请求,这样就一定程度上减少了leader的负荷。