目录html
疯狂创客圈 Java 分布式聊天室【 亿级流量】实战系列之 -25【 博客园 总入口 】java
@面试
你们好,我是做者尼恩。目前和几个小伙伴一块儿,组织了一个高并发的实战社群【疯狂创客圈】。正在开始高并发、亿级流程的 IM 聊天程序 学习和实战算法
前面,已经完成一个高性能的 Java 聊天程序的四件大事:数据库
接下来,须要进入到分布式开发的环节了。 分布式的中间件,疯狂创客圈的小伙伴们,一致的选择了zookeeper,不单单是因为其在大数据领域,太有名了。更重要的是,不少的著名框架,都使用了zk。apache
本篇介绍 ZK 的分布式命名服务 中的 节点命名服务和 snowflake 雪花算法。服务器
前面讲到,在分布式集群中,可能须要部署的大量的机器节点。在节点少的受,能够人工维护。在量大的场景下,手动维护成本高,考虑到自动部署、运维等等问题,节点的命名,最好由系统自动维护。并发
节点的命名,主要是为节点进行惟一编号。主要的诉求是,不一样节点的编号,是绝对的不能重复。一旦编号重复,就会致使有不一样的节点碰撞,致使集群异常。框架
有如下两个方案,可供生成集群节点编号:运维
(1)使用数据库的自增ID特性,用数据表,存储机器的mac地址或者ip来维护。
(2)使用ZooKeeper持久顺序节点的次序特性。来维护节点的编号。
这里,咱们采用第二种,经过ZooKeeper持久顺序节点特性,来配置维护节点的编号NODEID。
集群节点命名服务的基本流程是:
(1)启动节点服务,链接ZooKeeper, 检查命名服务根节点根节点是否存在,若是不存在就建立系统根节点。
(2)在根节点下建立一个临时顺序节点,取回顺序号作节点的NODEID。如何临时节点太多,能够根据须要,删除临时节点。
基本的算法,和生成分布式ID的大部分是一致的,主要的代码以下:
package com.crazymakercircle.zk.NameService; import com.crazymakercircle.util.ObjectUtil; import com.crazymakercircle.zk.ZKclient; import lombok.Data; import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.CreateMode; /** * create by 尼恩 @ 疯狂创客圈 **/ @Data public class SnowflakeIdWorker { //Zk客户端 private CuratorFramework client = null; //工做节点的路径 private String pathPrefix = "/test/IDMaker/worker-"; private String pathRegistered = null; public static SnowflakeIdWorker instance = new SnowflakeIdWorker(); private SnowflakeIdWorker() { instance.client = ZKclient.instance.getClient(); instance.init(); } // 在zookeeper中建立临时节点并写入信息 public void init() { // 建立一个 ZNode 节点 // 节点的 payload 为当前worker 实例 try { byte[] payload = ObjectUtil.Object2JsonBytes(this); pathRegistered = client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(pathPrefix, payload); } catch (Exception e) { e.printStackTrace(); } } public long getId() { String sid=null; if (null == pathRegistered) { throw new RuntimeException("节点注册失败"); } int index = pathRegistered.lastIndexOf(pathPrefix); if (index >= 0) { index += pathPrefix.length(); sid= index <= pathRegistered.length() ? pathRegistered.substring(index) : null; } if(null==sid) { throw new RuntimeException("节点ID生成失败"); } return Long.parseLong(sid); } }
Twitter的snowflake 算法,是一种著名的分布式服务器用户ID生成算法。SnowFlake算法所生成的ID 是一个64bit的长整形数字。这个64bit被划分红四部分,其中后面三个部分,分别表示时间戳、机器编码、序号。
(1)第一位
占用1bit,其值始终是0,没有实际做用。
(2)时间戳
占用41bit,精确到毫秒,总共能够容纳约69年的时间。
(3)工做机器id
占用10bit,最多能够容纳1024个节点。
(4)序列号
占用12bit,最多能够累加到4095。这个值在同一毫秒同一节点上从0开始不断累加。
整体来讲,在工做节点达到1024顶配的场景下,SnowFlake算法在同一毫秒内最多能够生成多少个全局惟一ID呢?这是一个简单的乘法:
同一毫秒的ID数量 = 1024 X 4096 = 4194304
400多万个ID,这个数字在绝大多数并发场景下都是够用的。
snowflake 算法中,第三个部分是工做机器ID,能够结合上一节的命名方法,并经过Zookeeper管理workId,免去手动频繁修改集群节点,去配置机器ID的麻烦。
/** * create by 尼恩 @ 疯狂创客圈 **/ public class SnowflakeIdGenerator { /** * 单例 */ public static SnowflakeIdGenerator instance = new SnowflakeIdGenerator(); /** * 初始化单例 * * @param workerId 节点Id,最大8091 * @return the 单例 */ public synchronized void init(long workerId) { if (workerId > MAX_WORKER_ID) { // zk分配的workerId过大 throw new IllegalArgumentException("woker Id wrong: " + workerId); } instance.workerId = workerId; } private SnowflakeIdGenerator() { } /** * 开始使用该算法的时间为: 2017-01-01 00:00:00 */ private static final long START_TIME = 1483200000000L; /** * worker id 的bit数,最多支持8192个节点 */ private static final int WORKER_ID_BITS = 13; /** * 序列号,支持单节点最高每毫秒的最大ID数1024 */ private final static int SEQUENCE_BITS = 10; /** * 最大的 worker id ,8091 * -1 的补码(二进制全1)右移13位, 而后取反 */ private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); /** * 最大的序列号,1023 * -1 的补码(二进制全1)右移10位, 而后取反 */ private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS); /** * worker 节点编号的移位 */ private final static long APP_HOST_ID_SHIFT = SEQUENCE_BITS; /** * 时间戳的移位 */ private final static long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + APP_HOST_ID_SHIFT; /** * 该项目的worker 节点 id */ private long workerId; /** * 上次生成ID的时间戳 */ private long lastTimestamp = -1L; /** * 当前毫秒生成的序列 */ private long sequence = 0L; /** * Next id long. * * @return the nextId */ public Long nextId() { return generateId(); } /** * 生成惟一id的具体实现 */ private synchronized long generateId() { long current = System.currentTimeMillis(); if (current < lastTimestamp) { // 若是当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,出现问题返回-1 return -1; } if (current == lastTimestamp) { // 若是当前生成id的时间仍是上次的时间,那么对sequence序列号进行+1 sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == MAX_SEQUENCE) { // 当前毫秒生成的序列数已经大于最大值,那么阻塞到下一个毫秒再获取新的时间戳 current = this.nextMs(lastTimestamp); } } else { // 当前的时间戳已是下一个毫秒 sequence = 0L; } // 更新上次生成id的时间戳 lastTimestamp = current; // 进行移位操做生成int64的惟一ID //时间戳右移动23位 long time = (current - START_TIME) << TIMESTAMP_LEFT_SHIFT; //workerId 右移动10位 long workerId = this.workerId << APP_HOST_ID_SHIFT; return time | workerId | sequence; } /** * 阻塞到下一个毫秒 */ private long nextMs(long timeStamp) { long current = System.currentTimeMillis(); while (current <= timeStamp) { current = System.currentTimeMillis(); } return current; } }
上面的代码中,大量的使用到了位运算。
若是对位运算不清楚,估计很难看懂上面的代码。
这里须要强调一下,-1 的8位二进制编码为 1111 1111,也就是全1。
为何呢?
由于,8位二进制场景下,-1的原码是1000 0001,反码是 1111 1110,补码是反码加1。计算后的结果是,-1 的二进制编码为全1。16位、32位、64位的-1,二进制的编码也是全1。
上面用到的二进制位移算法,以及二进制按位或的算法,都比较简单。若是不懂,能够去查看java的基础书籍。
总的来讲,以上的代码,是一个相对比较简单的snowflake实现版本,关键的算法解释以下:
(1)在单节点上得到下一个ID,使用Synchronized控制并发,而非CAS的方式,是由于CAS不适合并发量很是高的场景。
(2)若是当前毫秒在一台机器的序列号已经增加到最大值4095,则使用while循环等待直到下一毫秒。
(3)若是当前时间小于记录的上一个毫秒值,则说明这台机器的时间回拨了,抛出异常。
(1)生成ID时不依赖于数据库,彻底在内存生成,高性能高可用。
(2)容量大,每秒可生成几百万ID。
(3)ID呈趋势递增,后续插入数据库的索引树的时候,性能较高。
(1)依赖于系统时钟的一致性。若是某台机器的系统时钟回拨,有可能形成ID冲突,或者ID乱序。
(2)还有,在启动以前,若是这台机器的系统时间回拨过,那么有可能出现ID重复的危险。
下一篇:基于zk,实现分布式锁。
Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战
疯狂创客圈 【 博客园 总入口 】