[平常]平常学习总结二

一  延时消息队列任务

1  延时消息队列介绍

(1)业务场景java

  例如在以下场景中可能会须要延时队列:git

    用户下订单成功以后隔20分钟给用户发送上门服务通知短信github

    订单完成一个小时以后通知用户对上门服务进行评价面试

    业务执行失败以后隔10分钟重试一次redis

  相似的场景比较多,简单的处理方式就是使用定时任务,假如数据比较多的时候,有的数据可能延迟比较严重,并且愈来愈多的定时业务致使任务调度很繁琐很差管理。算法

(2)开发前须要考虑的问题?sql

  1.及时性:消费端能按时收到数据库

  2.同一时间消息的消费权重apache

  3.可靠性:消息不能出现没有被消费掉的状况数组

  3.可恢复:假若有其余状况,致使消息系统不可用了,至少能保证数据能够恢复

  4.可撤回:由于是延迟消息,没有到执行时间的消息支持能够取消消费

  5.高可用、多实例:这里指HA/主备模式并非多实例同时一块儿工做

2  基于Redis的延迟队列

  选用redis做为数据缓存的主要缘由是由于redis自身支持zset的数据结构(score 延迟时间毫秒) 这样就少了排序的烦恼并且性能还很高,正好咱们的需求就是按时间维度去断定执行的顺序,同时也支持maplist数据结构。

    Zset本质就是Set结构上加了个排序的功能,除了添加数据value以外,还提供另外一属性score,这一属性在添加修改元素时候能够指定,每次指定后,Zset会自动从新按新的值调整顺序。

  能够理解为有两列字段的数据表,一列存value,一列存顺序编号。操做中key理解为zset的名字,那么对延时队列又有何用呢?

  试想若是score表明的是想要执行时间的时间戳,在某个时间将它插入Zset集合中,它变会按照时间戳大小进行排序,也就是对执行时间先后进行排序,这样的话,起一个死循环线程不断地进行取第一个key值,若是当前时间戳大于等于该key值的socre就将它取出来进行消费删除,就能够达到延时执行的目的, 注意不须要遍历整个Zset集合,以避免形成性能浪费。

/**
 * @program: test
 * @description: redis实现延时队列
 * @author: xingcheng
 * @create: 2018-08-19
 **/
public class DelayQueue {

    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static JedisPool jedisPool = new JedisPool(ADDR, PORT);
    private static CountDownLatch cdl = new CountDownLatch(10);

    public static Jedis getJedis() {
        return jedisPool.getResource();
    }

    /**
     * 生产者,生成5个订单
     */
    public void productionDelayMessage() {
        for (int i = 0; i < 5; i++) {
            Calendar instance = Calendar.getInstance();
            // 3秒后执行
            instance.add(Calendar.SECOND, 3 + i);
            DelayQueue.getJedis().zadd("orderId", (instance.getTimeInMillis()) / 1000, StringUtils.join("000000000", i + 1));
            System.out.println("生产订单: " + StringUtils.join("000000000", i + 1) + " 当前时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            System.out.println((3 + i) + "秒后执行");
        }
    }

    //消费者,取订单
    public static void consumerDelayMessage() {
        Jedis jedis = DelayQueue.getJedis();
        while (true) {
            Set<Tuple> order = jedis.zrangeWithScores("orderId", 0, System.currenTimeMillis(),0,1);//表示取出0-当前时间戳的数据,从第0个数据开始,取一个
            if (order == null || order.isEmpty()) {
                System.out.println("当前没有等待的任务");
                try {
                    TimeUnit.MICROSECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }
            Tuple tuple = (Tuple) order.toArray()[0];
            double score = tuple.getScore();
            Calendar instance = Calendar.getInstance();
            long nowTime = instance.getTimeInMillis() / 1000;
            if (nowTime >= score) {
                String element = tuple.getElement();
                Long orderId = jedis.zrem("orderId", element);
                if (orderId > 0) {
                    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + ":redis消费了一个任务:消费的订单OrderId为" + element);
                }
            }
        }
    }

    static class DelayMessage implements Runnable{
        @Override
        public void run() {
            try {
                cdl.await();
                consumerDelayMessage();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        DelayQueue appTest = new DelayQueue();
        appTest.productionDelayMessage();
        for (int i = 0; i < 10; i++) {
            new Thread(new DelayMessage()).start();
            cdl.countDown();
        }
    }
}
View Code

生产环境使用注意:

  因为这种实现方式简单,但在生产环境下大可能是多实例部署,因此存在并发问题,即缓存的查找和删除不具备原子性(zrangeWithScores和zrem操做不是一个命令,不具备原子性),会致使消息的屡次发送问题,这个问题的避免方法以下:

  1.能够采用单独一个实例部署解决(不具有高可用特性,容易单机出现故障后消息不能及时发送)

  2.采用redis的lua脚本进行原子操做,即原子操做查找和删除(实现难度大)

 参考:https://my.oschina.net/u/3266761/blog/1930360

3  基于环形数组实现

  环形的任务队列,由数组实现,数组中元素是Set<Task>,数组长度是3600:

  (1)环形队列,例如能够建立一个包含3600个slot的环形队列(本质是个数组)

  (2)任务集合,环上每个slot是一个Set<Task>

  同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。

    

 

  Task结构中有两个核心属性:

    1. Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务

    2. Task-Function:须要执行的任务指针

  启动一个Timer,每一个一秒钟在移动一个slot,那转一圈正好须要一个小时。

  如上图,当前Current Index指向第一格,当有延时消息到达以后,例如但愿3610秒以后,触发一个延时消息任务,只需:

    1. 计算这个Task应该放在哪个slot,如今指向1,3610秒以后,应该是第11格,因此这个Task应该放在第11个slot的Set<Task>中

    2. 计算这个Task的Cycle-Num,因为环形队列是3600格,这个任务是3610秒后执行,因此应该绕3610/3600=1圈以后再执行,因而Cycle-Num=1

  Current Index不停的移动,每秒移动到一个新slot,遍历slot中对应的Set<Task>,每一个Task看Cycle-Num是否是0:

    1. 若是不是0,说明还须要多移动几圈,将Cycle-Num减1

    2. 若是是0,说明立刻要执行这个Task了,取出Task-Funciton执行(能够用单独的线程来执行Task),并把这个Task从Set<Task>中删除。

  Netty中的工具类HashedWheelTimer的原理与这种环形的延迟队列类似。

  扩展:

  10w定时任务,如何高效触发超时  https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959957&idx=1&sn=a82bb7e8203b20b2a0cb5fc95b7936a5&chksm=bd2d07498a5a8e5f9f8e7b5aeaa5bd8585a0ee4bf470956e7fd0a2b36d132eb46553265f4eaf&scene=21#wechat_redirect

 二  分库分表相关问题

1  面试题

  为何要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不一样的分库分表中间件都有什么优势和缺点?大家具体是如何对数据库如何进行垂直拆分或水平拆分的?

2  考察点

  其实这块确定是扯到高并发了,由于分库分表必定是为了支撑高并发、数据量大两个问题的。并且如今说实话,尤为是互联网类的公司面试,基本上都会来这么一下,分库分表如此广泛的技术问题,不问实在是不行,而若是你不知道那也实在是说不过去!

3  为何要分库分表

  分库分表是两种不一样的处理方式,多是光分库不分表,也多是光分表不分库,也可能同时分库而且分表。

  例如以下场景:

  (1)假如咱们如今是一个小创业公司(或者是一个 BAT 公司刚兴起的一个新部门),如今注册用户就 20 万,天天活跃用户就 1 万,天天单表数据量就 1000,而后高峰期每秒钟并发请求最多就 10。这种状况单表数据量不大,而且并发量也很低,所以就不须要考虑分库分表,防止带来代码上的复杂性。

  (2)若是业务发展迅猛,过了几个月,注册用户数达到了 2000 万!天天活跃用户数 100 万!天天单表数据量 10 万条!高峰期每秒最大请求达到 1000!由于天天多 10 万条数据,一个月就多 300 万条数据,如今我们单表已经几百万数据了,立刻就破千万了,可是勉强还能撑着。高峰期请求如今是 1000,我们线上部署了几台机器,负载均衡搞了一下,数据库撑 1000QPS 也还凑合。可是这种状况下就须要考虑下分库分表了。

  (3)再接下来几个月,公司用户数已经达到 1 亿,,由于此时天天活跃用户数上千万,天天单表新增数据多达 50 万,目前一个表总数据量都已经达到了两三千万了!扛不住啊!数据库磁盘容量不断消耗掉!高峰期并发达到惊人的 5000~8000!此时系统就会撑不住而致使宕机。

  所以,其实是否须要分库分表这是跟着你的公司业务发展走的,你公司业务发展越好,用户就越多,数据量越大,请求量越大,那你单个数据库必定扛不住。

4  分表

  好比你单表都几千万数据了,你肯定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。通常来讲,就以个人经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。

  分表是啥意思?就是把一个表的数据放到多个表中,而后查询的时候你就查一个表。好比按照用户 id 来分表,将一个用户的数据就放在一个表中。而后操做的时候你对一个用户就操做那个表就行了。这样能够控制每一个表的数据量在可控的范围内,好比每一个表就固定在 200 万之内。

5  分库

  分库是啥意思?就是你一个库通常咱们经验而言,最多支撑到并发 2000,必定要扩容了,并且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你能够将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

  这就是所谓的分库分表,为啥要分库分表?你明白了吧。

6  经常使用中间件

  Sharding-jdbc

  当当开源的,属于 client 层方案,目前已经改名为 ShardingSphere(后文所提到的 Sharding-jdbc,等同于 ShardingSphere)。确实以前用的还比较多一些,由于 SQL 语法支持也比较多,没有太多限制,并且截至 2019.4,已经推出到了 4.0.0-RC1 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。并且确实以前使用的公司会比较多一些(这个在官网有登记使用的公司,能够看到从 2017 年一直到如今,是有很多公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,我的认为算是一个如今也能够选择的方案。

  Mycat

  基于 Cobar 改造的,属于 proxy 层方案,支持的功能很是完善,并且目前应该是很是火的并且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。可是确实相比于 Sharding jdbc 来讲,年轻一些,经历的锤炼少一些。

  总结:

  综上,如今其实建议考量的,就是 Sharding-jdbc 和 Mycat,这两个均可以去考虑使用。

  Sharding-jdbc 这种 client 层方案的优势在于不用部署,运维成本低,不须要代理层的二次转发请求,性能很高,可是若是遇到升级啥的须要各个系统都从新升级版本再发布,各个系统都须要耦合 Sharding-jdbc 的依赖;

  Mycat 这种 proxy 层方案的缺点在于须要单独部署,本身运维一套中间件,运维成本高,可是好处在于对于各个项目是透明的,若是遇到升级之类的都是本身中间件那里搞就好了。

  一般来讲,这两个方案其实均可以选用,可是我我的建议中小型公司选用 Sharding-jdbc,client 层方案轻便,并且维护成本低,不须要额外增派人手,并且中小型公司系统复杂度会低一些,项目也没那么多;可是中大型公司最好仍是选用 Mycat 这类 proxy 层方案,由于可能大公司系统和项目很是多,团队很大,人员充足,那么最好是专门弄我的来研究和维护 Mycat,而后大量项目直接透明使用便可。

7  如何对数据库进行垂直拆分或水平拆分

   水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,可是每一个库的表结构都同样,只不过每一个库表放的数据是不一样的,全部库表的数据加起来就是所有数据。水平拆分的意义,就是将数据均匀放更多的库里,而后用多个库来扛更高的并发,而且能够用多个库的存储容量来进行扩容

 

  垂直拆分的意思,就是把一个有不少字段的表给拆分红多个表,或者是多个库上去。每一个库表的结构都不同,每一个库表都包含部分字段。通常来讲,会将较少的访问频率很高的字段放到一个表里去,而后将较多的访问频率很低的字段放到另一个表里去。由于数据库是有缓存的,你访问频率高的行字段越少,就能够在缓存里缓存更多的行,性能就越好。这个通常在表层面作的较多一些。

 

  这个其实挺常见的,不必定我说,你们不少同窗可能本身都作过,把一个大表拆开,订单表、订单支付表、订单商品表。

  还有表层面的拆分,就是分表,将一个表变成 N 个表,就是让每一个表的数据量控制在必定范围内,保证 SQL 的性能。不然单表数据量越大,SQL 性能就越差。通常是 200 万行左右,不要太多,可是也得看具体你怎么操做,也多是 500 万,或者是 100 万。你的SQL越复杂,就最好让单表行数越少

  好了,不管分库仍是分表,上面说的那些数据库中间件都是能够支持的。就是基本上那些中间件能够作到你分库分表以后,中间件能够根据你指定的某个字段值,好比说 userid,自动路由到对应的库上去,而后再自动路由到对应的表里去。

你就得考虑一下,你的项目里该如何分库分表?

  通常来讲,垂直拆分,你能够在表层面来作,对一些字段特别多的表作一下拆分;

  水平拆分,你能够说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你本身想好;

  分表,你考虑一下,你若是哪怕是拆到每一个库里去,并发和容量都 ok 了,可是每一个库的表仍是太大了,那么你就分表,将这个表分开,保证每一个表的数据量并非很大。

并且这儿还有两种分库分表的方式:

  (1)一种是按照 range 来分,就是每一个库一段连续的数据,这个通常是按好比时间范围来的,可是这种通常较少用,由于很容易产生热点问题,大量的流量都打在最新的数据上了。

  (2)按照某个字段 hash 一下均匀分散,这个较为经常使用。

  range 来分,好处在于说,扩容的时候很简单,由于你只要预备好,给每月都准备一个库就能够了,到了一个新的月份的时候,天然而然,就会写新的库了;缺点,可是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

  hash 分发,好处在于说,能够平均分配每一个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,以前的数据须要从新计算 hash 值从新分配到不一样的库或表。

  参考:https://github.com/yuejuntao/advanced-java/blob/master/docs/high-concurrency/database-shard.md

三  如何设计让系统从未分库分表动态切换到分库分表上

1  面试题

  如今有一个未分库分表的系统,将来要分库分表,如何设计才可让系统从未分库分表动态切换到分库分表上?

2  考察点

  如今已经明白为啥要分库分表了,你也知道经常使用的分库分表中间件了,你也设计好大家如何分库分表的方案了(水平拆分、垂直拆分、分表),那问题来了,你接下来该怎么把你那个单库单表的系统给迁移到分库分表上去?

  因此这都是一环扣一环的,就是看你有没有全流程经历过这个过程。

3  解析

  1  停机迁移方案

  最简单的方案,在凌晨 12 点开始运维,网站或者 app 挂个公告,说 0 点到早上 6 点进行运维,没法访问。接着到 0 点停机,系统停掉。

  因为没有新的流量写入了,所以此时老的单库单表数据库的数据不会发生变化。此时能够经过开发的数据传输工具,而后将单库单表的数据读出来,写到分库分表里面去。

  导数完了以后,修改系统的数据库链接配置,包括可能代码和 SQL 也许有修改,那你就用最新的代码,而后直接启动连到新的分库分表上去。

  

  如图所示,具体步骤以下:

  (1)系统停机,不容许外界访问

  (2)经过后台程序将老的单表单库中的数据按照按照分库分表的规则导入到新的分库分表中。

  (3)修改系统配置及分库分享相关SQL,使系统链接到新的数据库上,而且以后新的数据都写入到新的分库分表中。

  (4)启动系统,运行外界访问。

  2  双写迁移方案

  在停机迁移方案中,须要使系统中止运行一段时间,这有时候对线上是没法接受的,所以能够采用双写迁移方案,不须要停机。 

  简单来讲,就是在线上系统里面,以前全部写库的地方,增删改操做,除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。此时查询仍然从老的库中查询。

  而后系统部署以后,新库数据差太远,用后台程序跑起来读老库数据写入到新库,写的时候要根据 modified_time 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来讲,就是不容许用老数据覆盖新数据。

  导完一轮以后,有可能数据仍是存在不一致,那么就程序自动作一轮校验,比对新老库每一个表的每条数据,接着若是有不同的,就针对那些不同的,从老库读数据再次写。反复循环,直到两个库每一个表的数据都彻底一致为止。

  接着当数据彻底一致了,就切换读数据重新库中读取,同时断开老数据库链接。

  

  参考:https://github.com/yuejuntao/advanced-java/blob/master/docs/high-concurrency/database-shard-method.md

四  分库分表后,id主键如何处理

1  面试题

   分库分表以后,id 主键如何处理?

2  考察点

  其实这是分库分表以后你必然要面对的一个问题,就是 id 咋生成?

  由于要是分红多个表以后,每一个表都是从 1 开始累加,那确定不对啊,须要一个全局惟一的 id 来支持。因此这都是你实际生产环境中必须考虑的问题。

3  解析

  1  基于数据库的实现方案

  (1)数据库自增id 

  这个就是说你的系统里每次获得一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,而后获取一个数据库自增的一个 id。拿到这个 id 以后再往对应的分库分表里去写入。

  例如名为table的表结构以下:

    id  field

    35  a

  每一次生成id的时候,都访问数据库,执行以下语句:

begin;
  REPLACE INTO table ( feild )  VALUES ( 'a' );
  SELECT LAST_INSERT_ID();
commit;

  REPLACE INTO 的含义是插入一条记录,若是表中惟一索引的值遇到冲突,则替换老数据。

  这样一来,每次均可以获得一个递增的ID。

  这个方案的好处就是方便简单;缺点就是单库生成自增 id,要是高并发的话,就会有瓶颈的

  若是改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,而后本身递增几个 id,一次性返回一批 id,而后再把当前最大 id 值修改为递增几个 id 以后的一个值;可是不管如何都是基于单个数据库。

  适合的场景:你分库分表无非两个缘由:(1)单库并发过高,(2)单库数据量太大;除非是你并发不高,可是数据量太大致使的分库分表扩容,你能够用这个方案,由于可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键便可。

   (2)设置数据库sequence或者表的自增字段步长

  因为使用数据库自增id只能基于单个数据库和表,所以在并发量高的状况下并不适用。此时能够经过设置数据库sequence(Oracle)或者表的自增字段来进行水平伸缩。

  例如:搭建8个数据库节点,每一个数据库中使用一个表来产生id。其中每一个数据库节点使用一个sequence功能来产生id,每一个sequence的起始ID不一样,而且依次递增,步长都是8:

  所以此时既能将单个库生成自增id扩展到多个库,增长了并发量。

  适合的场景:在用户防止产生的 ID 重复时,这种方案实现起来比较简单,也能达到性能目标。可是服务节点固定,步长也固定,未来若是还要增长服务节点,就很差搞了。

  2  UUID

  好处就是本地生成,不要基于数据库来了;

  很差之处就是,UUID 太长了、占用空间大,做为主键性能太差了;

  更重要的是,UUID 不具备有序性,会致使 B+ 树索引在写的时候有过多的随机写操做(连续的 ID 能够产生部分顺序写),还有,因为在写的时候不能产生有顺序的 append 操做,而须要进行 insert 操做,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操做在记录占用空间比较大的状况下,性能降低明显。

  例如以下B+树索引:

  

  若是咱们的ID按递增的顺序来插入,好比陆续插入8,9,10,新的ID都只会插入到最后一个节点当中。当最后一个节点满了,会裂变出新的节点。这样的插入是性能比较高的插入,由于这样节点的分裂次数最少,并且充分利用了每个节点的空间。

  可是,若是咱们的插入彻底无序,不但会致使一些中间节点产生分裂,也会白白创造出不少不饱和的节点,这样大大下降了数据库插入的性能。

  适合的场景:若是你是要随机生成个什么文件名、编号之类的,你能够用 UUID,可是做为主键是不能用 UUID 的。

  UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf

  3  获取系统当前时间

  这个就是获取当前时间便可,可是问题是,并发很高的时候,好比一秒并发几千,会有重复的状况,这个是确定不合适的。基本就不用考虑了。

  适合的场景:通常若是用这个方案,是将当前时间跟不少其余的业务字段拼接起来,做为一个 id,若是业务上你以为能够接受,那么也是能够的。你能够将别的业务字段值跟当前时间拼接起来,组成一个全局惟一的编号。

  4  snowflake算法

  snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id分红四部分:1 个 bit 是不用的,用其中的 41 bit 做为毫秒数,用 10 bit 做为工做机器 id,12 bit 做为序列号。

   (1)1 bit:不用,为啥呢?由于二进制里第一个 bit 为若是是 1,那么都是负数,可是咱们生成的 id 都是正数,因此第一个 bit 统一都是 0。

   (2)41 bit:表示的是时间戳,单位是毫秒。41 bit 能够表示的数字多达 2^41 - 1,也就是能够标识 2^41 - 1 个毫秒值,换算成年就是表示69年的时间。

   (3)10 bit:记录工做机器 id,表明的是这个服务最多能够部署在 2^10台机器上哪,也就是1024台机器。可是 10 bit 里 5 个 bit 表明机房 id,5 个 bit 表明机器 id。意思就是最多表明 2^5个机房(32个机房),每一个机房里能够表明 2^5 个机器(32台机器)。

   (4)12 bit:这个是用来记录同一个毫秒内产生的不一样 id,12 bit 能够表明的最大正整数是 2^12 - 1 = 4096,也就是说能够用这个 12 bit 表明的数字来区分同一个毫秒内的 4096 个不一样的 id。

public class IdWorker {
    //工做节点id(0~31)
    private long workerId;
  //数据中心id(0~31)    
  private long datacenterId;
  //毫秒内序列(0~4095) 
   private long sequence;

  //构造函数
    public IdWorker(long workerId, long datacenterId, long sequence) {
        // sanity check for workerId
        // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(
                    String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(
                    String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        System.out.printf(
                "worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

  //初始时间戳
    private long twepoch = 1288834974657L;
  //机器id所占位数
    private long workerIdBits = 5L;
  //数据标识id所占位数
    private long datacenterIdBits = 5L;

    // 这个是二进制运算,就是 5 bit最多只能有31个数字,也就是说机器id最多只能是32之内
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);

    // 这个是一个意思,就是 5 bit最多只能有31个数字,机房id最多只能是32之内
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
  //序列在id中所占的位数
    private long sequenceBits = 12L;
  //机器id的偏移量(12)
    private long workerIdShift = sequenceBits;
  //机房id偏移量(12+5)
    private long datacenterIdShift = sequenceBits + workerIdBits;
  //时间戳偏移量(5+5+12)
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
  //生成的序列的掩码,治理为4095(0b111111111111=0xfff=4095)
    private long sequenceMask = -1L ^ (-1L << sequenceBits);
  //上次生成id的时间戳
    private long lastTimestamp = -1L;

    public long getWorkerId() {
        return workerId;
    }

    public long getDatacenterId() {
        return datacenterId;
    }

    public long getTimestamp() {
        return System.currentTimeMillis();
    }

  //获取下一个id(用同步锁保证线程安全)
    public synchronized long nextId() {
        // 这儿就是获取当前时间戳,单位是毫秒
        long timestamp = timeGen();
     //若是当前时间小于上一次id生成的时间戳,则说明系统时钟回退过,这个时候抛出异常
        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format(
                    "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            //若是是同一时间生成的,则进入毫秒内序列 
        //这个意思是说一个毫秒内最多只能有4096个数字
            // 不管你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你本身传递个sequence超过了4096这个范围
            sequence = (sequence + 1) & sequenceMask;
       //sequence等于0说明毫秒内序列已经增加到最大值
            if (sequence == 0) {
         //阻塞到下一个毫秒,获取新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
        lastTimestamp = timestamp;

        // 这儿就是将时间戳左移,放到 41 bit那儿;
        // 将机房 id左移放到 5 bit那儿;
        // 将机器id左移放到5 bit那儿;将序号放最后12 bit;
        // 最后拼接起来成一个 64 bit的二进制数字,转换成 10 进制就是个 long 型
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;
    }

//阻塞到下一个毫秒,知道获取到新的时间戳
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    // ---------------测试---------------
    public static void main(String[] args) {
        IdWorker worker = new IdWorker(1, 1, 1);
        for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }
    }

}
View Code

说明:

  1.得到单一机器的下一个序列号,使用Synchronized控制并发,而非CAS的方式,是由于CAS不适合并发量很是高的场景。

  2.若是当前毫秒在一台机器的序列号已经增加到最大值4095,则使用while循环等待直到下一毫秒。

  3.若是当前时间小于记录的上一个毫秒值,则说明这台机器的时间回拨了,抛出异常。但若是这台机器的系统时间在启动以前回拨过,那么有可能出现ID重复的危险。

SnowFlake算法的优势:

  1.生成ID时不依赖于DB,彻底在内存生成,高性能高可用。

  2.ID呈趋势递增,后续插入索引树的时候性能较好。

SnowFlake算法的缺点:

  依赖于系统时钟的一致性。若是某台机器的系统时钟回拨,有可能形成ID冲突,或者ID乱序。

参考:https://mp.weixin.qq.com/s/JiyZbaAujBtD8F4ddc-uAw

五  count(1)比count(*)效率高吗

  有 Where 条件的 count,会根据扫码结果count 一下全部的行数,其性能更依赖于你的 Where 条件,因此文章咱们仅针对没有 Where 的状况进行说明。

  MyISAM 引擎会把一个表的总行数记录了下来,因此在执行 count(*) 的时候会直接返回数量,执行效率很高。

  在 MySQL 5.5 之后默认引擎切换为 InnoDB,InnoDB 由于增长了版本控制(MVCC)的缘由,同时有多个事务访问数据而且有更新操做的时候,每一个事务须要维护本身的可见性,那么每一个事务查询到的行数也是不一样的,因此不能缓存具体的行数,他每次都须要 count 一下全部的行数。那么 count(1) 和 count(*)有区别么?

  Returns a count of the number of non-NULL values of expr in the rows retrieved by a SELECT statement. The result is a BIGINT value.

  大体的解释是返回 SELECT 语句检索的行中 expr 的非 NULL 值的计数,到这里咱们就明白了,首先它是一个聚合函数,而后对 SELECT 的结果集进行计数,可是须要参数不为 NULL。那么咱们继续阅读官网的内容:

  COUNT(*) is somewhat different in that it returns a count of the number of rows retrieved, whether or not they contain NULL values.

  大体的内容是说,count(*) 不一样,他不关心这个返回值是否为空都会计算他的count,由于 count(1) 中的 1 是恒真表达式,那么 count(*) 仍是 count(1) 都是对全部的结果集进行 count,因此他们本质上没有什么区别。

  到这里咱们明白了 count(*) 和 count(1) 本质上面实际上是同样的,那么 count(column) 又是怎么回事呢?

  count(column) 也是会遍历整张表,可是不一样的是它会拿到 column 的值之后判断是否为空,而后再进行累加,那么若是针对主键须要解析内容,若是是二级因此须要再次根据主键获取内容,又是一次 IO 操做,因此 count(column) 的性能确定不如前二者喽,若是按照效率比较的话:

  count(*)=count(1)>count(primary key)>count(column)

  既然 count(*) 在查询上依赖于全部的数据集,是否是咱们在设计上也须要尽可能的规避全量 count 呢?一般状况咱们针对可预见的 count 查询会作适当的缓存,能够是 Redis,也能够是独立的 MySQL count 表,固然不管是哪一种方式咱们都须要考虑一致性的问题。