答疑一

1.mysql分库分表的情况下,主键id都是基于雪花算法实现的吗?雪花算法是如何保证id唯一的?

mysql分库分表的情况下,主键id都是基于雪花算法实现的吗?雪花算法是如何保证id唯一的?

雪花算法是什么?

雪花算法在分布式架构中比较常见,是用来生成全局唯一ID的,说到全局唯一ID,不得不提到UUID,它是Java自带的生成一串唯一随机36位字符串(32个字符串+4个“-”)的算法。它可以保证唯一性,且据说够用N亿年,但是其业务可读性差,无法有序递增,它是随机生成的。snowflake(雪花算法)是有序递增生成的
SnowFlake是Twitter公司采用的一种算法,目的是在分布式系统中产生全局唯一且趋势递增的ID。
![]([object Object]&originHeight=221&originWidth=640&size=0&status=done&style=none&width=640)

组成部分(64bit)

1.第一位 占用1bit,其值始终是0,没有实际作用。 2.时间戳 占用41bit,精确到毫秒,总共可以容纳约69年的时间。 3.工作机器id 占用10bit,其中高位5bit是数据中心ID,低位5bit是工作节点ID,做多可以容纳1024个节点。 4.序列号 占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生4096个ID。
SnowFlake算法在同一毫秒内最多可以生成多少个全局唯一ID呢:: 同一毫秒的ID数量 = 1024 X 4096 = 4194304
**
雪花算法是分布式ID的一种解决方案,除了雪花算法之外还有等下的主角预生成取号器,那么分布式ID首先要满足下面的特点:

分布式ID的特点

全局唯一性
不能出现有重复的ID标识,这是基本要求。
递增性
确保生成ID对于用户或业务是递增的。
高可用性
确保任何时候都能生成正确的ID。
高性能性
在高并发的环境下依然表现良好。

分布式ID的常见解决方案

  • UUID
  • SnowFlake
  • UidGenerator
  • Leaf
  • 预生成取号器

预生成取号器

上面的snowflake算法是一种逻辑算法来确保ID全局唯一,是通过时间戳+机器码+序列号三部分组成的,这种方案有什么劣势呢?其实上面已经表现出来了,64bit,8个字节用来存储全局ID,比较浪费空间,如果是一个订单系统,订单量比较大,那么占用空间就会呈现直线上升,那么如何解决这个问题呢?
这个就是预生成取号器方案了,他并不是一个逻辑算法来确保全局唯一的,而是通过一个取号器,也就是一个取号服务来确保的,不过它并不是一个绝对顺序,而是一个相对顺序的概念。取号器一般为了高性能高可用会分布式部署,比如部署三台取号器服务,那么这三台会在内存中预生成一些ID号,比如10W个号段,那么这三台分别是第一台1-10W,第二台10W-20W,第三台20W-30W,这样当应用服务需要生成ID的时候只需要去取号器中去取就可以了。
这些号段在取号器服务中可以以范围的存储,也可以生成每个号段然后放入队列中,然后以消费者模型,每个ID被取出后即被消费,这样就避免了重复消费的问题。
看到这里是否产生一个疑问,取号器分布式部署,每个服务内的ID是唯一的,那么如何保障多个服务之间的服务是唯一的?这个只需要这些服务连接同一个存储介质就可以了,比如mysql、redis,当然redis会出现丢失的情况,使用mysql就可以,使用mysql记录当前全局ID的上限,这样当取号器去预生成一批全局ID的时候就知道生成什么范围的全局ID是不会重复的了。
预生成ID,这里有两个概念,一、一次性生成多少个全局ID,二、什么时候去预生成全局ID?
预生成全局ID这个要根据具体业务量情况而定,并不是预生成的越多越好,首先如果预生成的过多并且未知原因下这个取号器服务宕机了,那么这些全局ID也会有丢失的风险,因为本身来讲这些全局ID是放在内存中的。
什么时候去预生成?既然是预生成肯定不是用完后再去生成,这样也会导致Id到达临界点后服务响应变慢,所以也同样可以根据业务的情况去预生成,比如当使用达到2/3的使用再去预生成一批。
什么说预生成方案下,全局ID是相对顺序?
首先取号器服务为了三高标准进行分布式部署,那么每个取号器服务取到全局ID号段都是不相同的,当应用服务需要生成全局ID的时候,会使用RPC框架调用取号器服务生成,可是具体调用哪个取号器服务却是根据调度机制来决定的,这样的话,虽然可能前后两个订单,但是由于调用两个不同的取号器,可能这两个全局ID相差10W的距离。

在分库场景下,既然使用全局唯一ID,每个分库表的primary自增主键为什么还是需要需要设置?要知道这些自增主键是会出现重复的。
因为innodb的聚簇索引,使用自增主键做聚簇索引,提升索引效率

参考:https://zhuanlan.zhihu.com/p/85837641

2.分库分表后发现有的查询场景用不上分片键怎么办?使用分片键可以包住所有的C端查询请求,但是B端查询请求有些是包不住的,这样合理么?

在选取分片键的时候选择查询场景中使用较多的字段即可,可能会出现上面的情况,一些场景下分片键无法满足查询条件,这个是用可以两种方式来实现。
比如分片键是业务ID,现在希望使用手机号来进行查询

  1. 可以在缓存中做一个业务ID和手机号之间的映射,这样可以在使用手机号的时候进行一个转换
  2. 使用ES来进行异构

概念认知:

  • 富查询 :对数据的某一个属性进行查询获取所有满足条件的数据,例如所有颜色为红色的汽车信息。
  • 区间查询:对一个范围内的键值进行查询获取数据,例如获取单号在005至008之间的订单信息。

3.实现Serializable接口,需要定义一个serialVersionUID,这个serialVersionUID具体作用是干嘛的?

首先了解一下Serializable接口,实现该标记接口来实行Java对象的序列化,进而可以实现持久化缓存或者进行网络传输等操作。当然现在生产环境中使用对象转json的方式进行网络传输也是比较多的。
serialVersionUID是用来版本标记的,使用它来进行版本一致性管理,防止出现版本不一致却无法发现的问题。

  • 通过 Serializable 来实现序列化是根据serialVersionUID是否相同来判断序列化对象的版本是否一致。在反序列化时,当JVM判断出字节流中的serialVersionUID与当前class文件中的serialVersionUID不同时就会抛出InvalidClassException异常并返回null;
  • 当没有指定serialVersionUID的值的时候,JVM也会根据类名、成员方法及属性名自动生成serialVersionUID,但是这样的话就比较消耗性能了

具体一点的场景就是:

现有一个classA,它有两个属性,分别是name、age,实例化这个类并将它序列化存储在本地介质中。
后来又在classA中添加了一个属性sex,想要之前序列化的数据再反序列化到这个新类中,会是什么结果呢?肯定是报错了,为什么?因为JVM会判断出这两个classA的serialVersionUID不一致,无法反序列化。
那么如何可以做到反序列化呢?因为不想放弃存储中的数据,这个只有一个办法就是手动修改新的classA和旧的classA的serialVersionUID一致才可以

参考:

答疑三

1.线上问题有时候不是必然复现,我们怎么去定位问题呢?而且线上环境也不能随意去发请求?

答:首先先确定一下问题是什么类型的?一般bug可以分为两个类问题,一种是技术问题;一种是业务问题。
**技术问题,**比如CPU利用率飙升、内存满了,负载过高了。top查看线程级别的CPU利用率,然后结合jstack定位具体是哪个线程,进一步确认问题。如果是内存问题的,dump出来,mat分析。
**业务问题,**系统功能没有按预期运行。针对于业务问题最好的办法就是添加前置日志,在写代码和代码review的时候,尽可能的增加日志,只是增加的日志会有不同级别,比如info、error、warn以及debug等。可以肯定的是生产环境中输出太多日志本身是影响性能的,这个可以利用微服务架构中的配置中心来实现动态配置,比如正常运行的时候只打印error级别的信息,当出现功能不正常的时候,开启info或者debug,这样就可以避免打印过多日志尴尬情况了,这个也是一般做法。
但是如果在问题出现时没有日志埋点比较少,这就有些尴尬了,有如下几个方法:

  • 临时上一个版本,增加日志
  • 申请网络端口,进行远程debug
  • 有灰度环境的话  可以用btrace

2.缓存一致性问题该怎么解决?

详细

缓存一致性问题该怎么解决?无论是先删除/更新缓存,还是先更新数据库,在并发的情况下都有可能发生不一致的情况。网上查了说通常采用延时双删的方案,不是很能get到。是不是缓存一致性问题还是得考虑具体业务场景?

答:这个其实本身是个分布式事务问题,如何实现多个数据源的原子性操作的问题。有三个方案
第一个方案:
在业务可以允许容忍数据暂时的不一致的话,那么可以直接使用缓存过期时间,在更新数据库后更新缓存,如果缓存更新失败也没关系,只是暂时性的缓存和数据库的不一致,待缓存过期后,自然会更新一致性数据。
第二个方案:
延时双删+过期时间
_ 延时双删策略_
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。
关于这里要特别注意,一旦休眠时间设置的过短就会导致在极端情况下,未能将读操作的脏数据删除掉,但是有过期时间保障,只是暂时性的数据不一致。
设置缓存的过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
如何写完数据库后,再次删除缓存成功?
上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。
1、方案一具体流程
(1)更新数据库数据;
(2)缓存因为种种问题删除失败;
(3)将需要删除的key发送至消息队列;
(4)自己消费消息,获得需要删除的key;
(5)继续重试删除操作,直到成功。
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
第三方案:
canal+binlog数据同步
具体流程
(1)更新数据库数据;
(2)数据库会将操作信息写入binlog日志当中;
(3)订阅程序提取出所需要的数据以及key;
(4)另起一段非业务代码,获得该信息;
(5)尝试删除缓存操作,发现删除失败;
(6)将这些信息发送至消息队列;
(7)重新从消息队列中获得该数据,重试操作。
这个订阅程序可以选择阿里开源框架canal,进行数据库和缓存之间的数据同步,对业务数据实现零侵入。
参考:https://cloud.tencent.com/developer/news/634004

3.mysql的insert buffer thread是干什么的?

关于这部分问题,深入mysql架构,暂时不整理,待理清楚其中细节再整理。