线上故障之-雪花算法重复、序列化、redis锁失效、double精准计算

  • 雪花算法重复
    • 问题发现
    • 定位
    • 影响范围
    • 解决方案
    • 扩展-雪花算法原理分析:
  • 序列化
    • 运营商 POP 裸机搭售自营套餐事故分析
    • 直播抽奖超发奖品
      • 代码严谨性
      • 核账
      • 问题分析
  • redis锁失效
    • Redlock
      • Redlock 算法介绍
      • 原理
      • Redlock 算法是否安全
  • double 精准计算

雪花算法重复

问题发现

查看服务A发布项机器日志,发现表的insert报主键冲突异常

查看发号器服务器日志,发现两台机器产生相同的uniqueID

定位

1.发号器生成算法-雪花算法
该项目所采用雪花算法规则,64位生成的唯一标识,由4部分构成:1位符号位,41位的
diffTime,5位的datacenter,5位的workerId,12位的sequence
diffTime:当前的时间戳
datacenterId:当前机器IP对应唯一数字,2台机器IP分别为:xxx.xxx.xxx.94,xxx.xxx.xxx.95(配置中心配置的映射关系)
workerId:统一配置为0,无意义
sequence:自增序列,保证同一台主机同一时间戳下生成的唯一值

雪花算法生成规则 机器IP来源于配置中心
服务部署在两台机器上,两台机器IP分别为:xxx.xxx.xxx.94,xxx.xxx.xxx.95.配置中心配置的正常配置应该如比如{“xxx.xxx.xxx.94”:0,“xxx.xxx.xxx.95”:0}
配置中心缺少配置,因此雪花算法中的datacenterId都是用默认值31,因此两台机器在同一毫秒生成的ID会重复

影响范围

所有依赖该服务的应用

解决方案

1:方案1:应用中自定义算法工具类,优点:不依赖该雪花算法服务,避免修改雪花算法对其它服务产生影响。缺点:定义新规则开发雪花算法
2:方案2;修改配置中心,重启雪花算法服务。优点:不需要修改代码 缺点:对其依赖产生影响

方案一:1位符号位+ 41时间戳位 + 10位进程位 + 12位自增序列位
进程位 依赖 数据库自增主键,自增主键 与 1024取模。每次重启应用,获取 进程位,缓存到本地内存中。
心跳续约,如果过了一段时间没有续约,认为应用下线。
优点:不受扩缩容影响。且考虑同一发布项不会超过50台,不需要太关注时钟回拨问题,只需要简单校验即可。
缺点:本地缓存过期时间固定,更新进程位实时性不高。
方案二:
已经熟悉 Snowflake 的朋友可以先去看大厂的设计和权衡。
百度 UIDGenertor:https://github.com/baidu/uidgenerator/blob/master/README.zh_cn.md
美团 Leaf:Leaf:美团分布式ID生成服务开源 - 美团技术团队 (meituan.com)
腾讯 Seqsvr: https://www.infoq.cn/article/wechat

优点:既能自动扩缩容,也考虑到时钟回拨。
缺点:实现复杂,需要依赖外部资源。

扩展-雪花算法原理分析:

twitter版本雪花算法源码分析:https://github.com/twitter-archive/snowflake/tags
本文采用java版本:
为什么需要分布式ID?分布式ID的可选方案,优缺点?

参考:Leaf——美团点评分布式ID生成系统 - 美团技术团队 (meituan.com)

设计考量:
java基础数据类型当中

int类型最大值10亿级别(数量级,明显不够用)
float和double,存在不精准运算,有效位数并不大,占用字节最大同long
综合考量:long类型数据规很大,足够应用在ID策略上了

负数如何存储
位运算:&。如果两个对应的二进制位上的数都是1,结果是1,其他都是0

0000 1000
0000 1010
结-----果
0000 1000

按位或:|。如果两个对应的二进制位上的数都是0,结果是0,其他都是1

0000 1000
0000 1010
结-----果
0000 1000

按位异或:^。如果两个对应的二进制位上的数字相同,则运算结果为0,其他都是1
左移:<<。把二进制数据在内存空间中向左边移动。左移n相当于乘以2的n次方,但要注意两点:1-左移带有符号位的,说明每个数据类型左移都有位数限制;2-左移后原来的值不变,移位后是一个新的值
补码:该数的原码除符号位外各位取反,然后在最后一位加1

-1L去除符号位原码

0000000000000000000000000000000000000000000000000000000000000001

取反:

1111111111111111111111111111111111111111111111111111111111111110

加1:

1111111111111111111111111111111111111111111111111111111111111111

雪花算法原理分析

前置条件:单机,机器中心:31机器号码:31
workID:可认为是机器特征号码,一般由两部分组成:数据中心+机器号,各占用5位。也就是最大值25*25=2^10=1024个值(0-1023)
单机情况下,如果不考虑时钟回拨,上图中,workID对于单机固定,12位序列化的变化范围为:2^12=4096个值(0-4095)
这么分析,单机该种算法,每一毫秒可产生4096个可用不重复的序列号
1s内4096*1000=4096000个。足够很多场景下使用了。
时间部分:

2^41/1000*60*60*24*365 = 69年

69年都不会重复,既然只能使用69年,我们系统中的时间,是从1970年开始的,所以设计上,设置一个起始时间,也就是项目开始的时间,目的为了使用更久的时间,
代码

   public synchronized long nextId() {long timestamp = timeGen();/*当前时间<上次时间,说明时钟回拨*/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) {//////sequenceMask=0000000000000000000000000000000000000000000000000000111111111111//如果sequence=0,说明://sequence + 1=0000000000000000000000000000000000000000000000000000111111111111//sequence =   0000000000000000000000000000000000000000000000000000111111111110//初始sequence= 0000000000000000000000000000000000000000000000000000000000000001/*保证sequence不会超过序列号所能容纳的最大值*/sequence = (sequence + 1) & sequenceMask;/*如果sequence==0,说明已经到了最大数量+1*/if (sequence == 0) {/*时间等待1毫秒*/timestamp = tilNextMillis(lastTimestamp);}} else {/*每一个毫秒从0开始生成*/sequence = 0;}/*记录上次生成序列的时间*/lastTimestamp = timestamp;/*构造最终的数据:时间戳+数据中心位+机器号位+序列*/return ((timestamp - twepoch) << timestampLeftShift) |(datacenterId << datacenterIdShift) |(workerId << workerIdShift) |sequence;}

if (timestamp < lastTimestamp) { //lastTimestamp,上次生成时间,本次生成时间
timestamp ,如果小于,说明时钟回拨
if (lastTimestamp == timestamp) {//说明两次生成ID的时间戳在同一毫秒内,否则,每一s从0
开始生成最后的序列号。
最终如何组成结果:
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
每一个号段向左移位置,在按位或,如下图:

第一步:时间 左移22位
0101111100100110010111110101000111111101110000000000000000000000
第二步:数据中心datacenterId 左移位17
0000000000000000000000000000000000000000001111100000000000000000
第三步:机器号码workerId 左移位12
0000000000000000000000000000000000000000000000011111000000000000
sequence占据后12位置
0000000000000000000000000000000000000000000000000000111111111111
第四步:跟sequence组合按位或
0101111100100110010111110101000111111101110111111111111111111111

这里解释了最终结果的构成,非常巧妙,综合了性能,变化点,生成了非常完美,不容易重合的一个long数字,那么问题只剩下,相同时间如何拿到后边的12位序列了

if (lastTimestamp == timestamp) {
//如果等走到这里:说明来给你个问题,第一:肯定生成过一次序列,那么sequence一定不可能从0开始
//sequenceMark=-1L ^ (-1L << sequenceBits)经过前面基础知识,可知
//sequenceMask=0000000000000000000000000000000000000000000000000000111111111111
//如果sequence=0,说明:
//sequence + 1= 0000000000000000000000000000000000000000000000000001000000000000
//sequence = 0000000000000000000000000000000000000000000000000000111111111111
//说明此时sequence=4095,及已经增长到最大值
//初始进入此条件的时候,sequence=
0000000000000000000000000000000000000000000000000000000000000000
//经过sequence = (sequence + 1) & sequenceMask;
//sequence=0000000000000000000000000000000000000000000000000000000000000001
sequence = (sequence + 1) & sequenceMask;
//如果此处等于零,相当于4095之后,在同一秒内再次需要生成序列,此时根据设计12位,已经不能在生成
了,所以,相当于系统调整了下时间,把当前时间修改到下一秒钟,参考tilNextMillis(lastTimestamp),不
难理解了。
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
}

存在的问题:
问题1:注意事项
雪花算法的设计等价于单台机器
不变(符号位置)+变化(时间戳)+不变(最大31)+不变(最大31)+变化(0-+4096)
多台机器
不变(符号位置)(不变)+变化(时间戳)(多机会重复)+(不变(最大31)()+不变(最大31))(组合不变)+变化(0-+4096)(可能重复)
等价于:
不变+不变+(变化)(自由组合)+不变
自由组合:可以分成2段,也可以是云主机一段,每个IP不通即可、
问题2:时间回拨
这种设计,严重依赖服务器的时间,但是时间,不仅是哲学家的难题,也是计算机领域的一大难题,至少linux上,存在时间同步等产生的时间跳跃问题,在分布式环境中,如果事件发生回拨,则很大概率产生重复的ID。
知道了原生的雪花算法,我们在理解美团和百度开源的算法其实就不难理解了;

序列化

运营商 POP 裸机搭售自营套餐事故分析

摘要 时间:12 月 2 日~12 月 3 日
需求:运营商 POP 裸机搭售自营套餐(Android)
问题:APP 自营合约机套餐数据未下发
原因:获取商品 sku 代码缺陷导致系统出现异常
影响:APP 自营合约机单量下降
应急措施:生产环境版本回滚
事故背景
原定于 11 月 28 日发布的【运营商 POP 裸机搭售自营套餐】需求,上线过程中 master 打包测试不通过,发现是本地缓存影响 6.5.3 版本以下的合约机套餐数据下发。经过和商祥 开发确认后,删除合约机 handler 的本地缓存处理,并于 12 月 1 日重新测试、发布上线。
事故过程简述
12 月 3 日业务方(3C 文旅事业部)反馈产品自营合约机下单量有所减少,接到产品反 馈之后对线上服务进行回滚操作,回滚之后系统恢复正常。同时进行问题排查,经分析问题 由新上线的功能中,获取商品 sku 部分代码出现异常导致。次日完成问题的修复和测试并 于 12 月 5 日重新上线。
事故原因分析
开发阶段:上游 http 接口返回的合约机的套餐数据,在反序列化为 Map对象的过程当中,使用的 Fastjson 工具会根据 json 对象中数值的长度,自动转 换为不同类型的 java 数值。在本次sku 的获取过程中,会将长度为 11 的 POP 商品 sku 转换为 Long 类型的数值,而长度为 7 的自营商品的 sku 则被自动转换为 Integer 类型。 在开发过程中,未能意识到该潜在的问题。在service 层代码中获取 sku 时候,使用了和 该方法中其他字段一样的获取方式,即强制类型转换。这种处理方式对于 POP 商品的 sku 是没问题的。但是当遇到自营商品 sku 的时候,出现将Integer 类型的数值转为 Long 类 型的情况,这样就触发了 java 的类型转换异常
(ClassCastException: java.lang.Integer cannot be cast to java.lang.Long),抛出异常之后,
后续代码不再执行,所以出现自营 商品合约机套餐不下发的情况;
自测阶段:只使用 PRD 文档中提供的 sku 进行了验证,忽略了对自营商品 sku 的验 证。
事故代码:

1)强制转换,遇到自营商品 sku 将抛出类型转换异常。
2)行原有异常捕获处理中将异常输出为 info级别,会导致线上出现异常信息不 打印。
修改后代码:
1) 添加开关控制,出现问题通过开关回滚;
2) 修改 sku 获取方式,不进行强转;
3) 将商品类型判断条件前置,减少修改影响范围;
4) 将异常日志类型从 log 改为 error,出现异常生产环境将打印出异常堆栈;
5) 添加缓存开关功能,如果出现缓存导致的问题,通过开关关闭缓存功能。
事故反思总结
本次事故主要有两方面原因导致,第一是开发过程中考虑不够全面,没能够提前识别到 不同类型商品的 sku,长度不同可能导致的问题;另一方面测试时只局限于 PRD 提供的测 试数据进行,没能够自主去查找自营商品数据进行全面的测试。如果上面提到的任一环节稍 加注意就不会出现此次事故。
通过对此次事故的反思,今后的开发及自测中需要注意以下几点:
1) 要建立开发过程中的代码交叉 review 机制并自觉坚持执行,提前排查运行期 可能发生的异常;
2)要求产品提供更多 sku,开发和测试有充足的 sku 验证;
3.)除了完成业务逻辑之外,单独开发回滚开关,做好秒级回滚的预案;
4)功能上线前提前、积极沟通好业务方产品配合上线走查,降低上线风险

直播抽奖超发奖品

“如果一个人因为怕被骂,而不去做他认为正确的事情,就是他达到上限的时候–高铁”
X日凌晨,接到客诉:购买礼品抽不出奖品,经定位发现 直接原因是没有建表,之后盲目的处理过程当中认为mq没发生重试,对账过程当中又没有核对数据明细, 错误的补发了金币,针对此次事故,分享出来一些犯过的错误,与大家共勉,引以为戒!!!
主要问题清单:

  • 核账
  • fastjson 序列化需注意问题
  • 日志规范
  • 代码严谨性

按时间顺序还原问题经过,内容如下:

代码严谨性

问题代码1-自动建表job
job方法片段:

    @Overridepublic ReturnT<String> execute(String... params) throws SQLException {logger.info("CreateTableJobHandler execute params->{}", (Object[]) params);Integer year = null;Integer month = null;try {year = Integer.parseInt(params[0]);month = Integer.parseInt(params[1]);tableTemplate.operate(year, month);logger.info("CreateTableJobHandler.execute success");} catch (Exception e) {logger.error("CreateTableJobHandler params error:{}", (Object[]) params);return ReturnT.ERROR;}return ReturnT.SUCCESS;} 

tableTemplate.operate 方法片段

    public void operate(Integer year, Integer month) throws SQLException {//月表 for (String monthValue : monthList) {Calendar calendar = DateUtils.getPreMonthCalendarByYearAndMonth(year, month);String monthString = DateUtils.getFormatedDateString(calendar.getTime(),DateUtils.FORMATE_YYYYMM);String sql = MessageFormat.format(monthValue, monthString);Integer result = (Integer) sqlService.executeSql(sql, getConnection());if (result.equals(-1)) {logger.error("sql error:{}", sql);}}
//天表 for (String dayValue : dayList) {List<Date> datesByYearAndMonth = DateUtils.getPreDatesByYearAndMonth(year, month);for (Date date : datesByYearAndMonth) {String dayString = DateUtils.getFormatedDateString(date,DateUtils.FORMATE_YYYYMMDD);String sql = MessageFormat.format(dayValue, dayString);Integer result = (Integer) sqlService.executeSql(sql, getConnection());if (result.equals(-1)) {logger.error("sql error:{}", sql);}}}}

job设计功能及缺陷
功能

  • 每月定期四次执行,如果入参为空,则建立下个月的表,年末顺延下一年度
  • 如果入参不为空,则建立指定指定参数下一个月的表,年月正确性校验
    缺陷
  • 如果年月是一个已经过期的,未处理,
    case,输入2021,4 则当月份执行可生成5月份表,到5月份的时候,6月份的表不会建立
    类似问题思考:方法入参校验,调用链路上方法分不清边界,参数完整性校验,异常抛出问题,都很值得思考

解决方案:建立有效的建表审查复查机制,用技术的角度解决认为疏忽的问题。

核账

当初问题发生后,运营的建议是补发金币,代码如下,先贴出来,我们后分析:

   @Overridepublic ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,ConsumeConcurrentlyContext context) {try {GiftSendMessage message = JSON.parseObject(msgs.get(0).getBody(),GiftSendMessage.class);UserInfo userInfo = new UserInfo();userInfo.setMemberId(String.valueOf(message.getFromId()));userInfo.setScId(message.getScId());userInfo.setAnchorScid(message.getAnchorScid());userInfo.setAnchorId(message.getAnchorId());userInfo.setGiftSendToId(message.getToId());DrawByGiftSendConfig draw =filter.getGiftIdBySend().get(String.valueOf(message.getGiftId()));GiftSendRequest request = new GiftSendRequest();request.setActivityId(draw.getActivityId());request.setBoxType(draw.getBoxType());request.setGiftOrderId(message.getGiftOrderId());request.setBatch(message.getAmount());request.setSendTime(message.getSendTime());request.setUserInfo(userInfo);request.setPaymentVersion(PAYMENT_VERSION);this.userDrawService.giftSend(request);LOG.info("DRAW BY GIFT SEND WITH SANTA CHECKOUT, MQ-MESSAGE={}, REQUEST={}",JSON.toJSONString(message),JSON.toJSONString(request));} catch (Exception e) {LOG.error("GIFT-SEND-QUEUE CONSUME FATAL ERROR ON PARSING, DROPPED, MSG={}, EXCEPTION = {}, EXCEPTION - MESSAGE = {}", JSON.toJSONString(msgs), e.getClass().getSimpleName(),e.getMessage(), e);}return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}

MQ消费者中有try catch块,核账的时候未详细的核对数据。其实mq已经发生重试,重试发生在有表的时候,执行逻辑正常,保证了数据完整性。
思考:分析过程不代表事实,所有故障,一般都发生在 ‘自以为的不科学‘,事实却从不说谎。
解决方案:建立行之有效的核账机制,用管理和制度的维度,杜绝不规范操作

问题分析

上面的代码,问题出现在

图中红色方框当中,用的序列化方式为 fastjson,此行代码会抛出异常,导致消费失败,进入重
试队列,且没有任何业务日志输出。MQ源码如下:

如果异常,返回 ConsumeConcurrentlyStatus.RECONSUME_LATER;
结论:无论是kafka,还是RocketMq,消费者方法参数中的MessageExt对象不能被 fastjson默认的方式序列化

原因:
环境:项目采用1.2.31 (最新版本1.2.78)
接下来,我们分析下fastjson序列化的完整过程
fastjson反序列化的方式默认为采用 get方法、is方法作为序列化属性 字段的,序列化流程如下:

其中:在获取对象序列化的时候,MessageExt中有返回 ByteBuffer的get方法,代码如下:

    public ByteBuffer getStoreHostBytes() {return socketAddress2ByteBuffer(this.storeHost);}//socketAddress2ByteBuffer public static ByteBuffer socketAddress2ByteBuffer(SocketAddress socketAddress) {ByteBuffer byteBuffer = ByteBuffer.allocate(8);return socketAddress2ByteBuffer(socketAddress, byteBuffer);}//socketAddress2ByteBuffer public static ByteBuffer socketAddress2ByteBuffer(SocketAddress socketAddress, ByteBufferbyteBuffer) {InetSocketAddress inetSocketAddress = (InetSocketAddress)socketAddress;byteBuffer.put(inetSocketAddress.getAddress().getAddress(), 0, 4);byteBuffer.putInt(inetSocketAddress.getPort());byteBuffer.flip();return byteBuffer;} 

Mq消息在接收到消息时,构造了返回了ByteBuffer对象的方法,该方法是nio中设计用于保存数
据到缓冲区的目的。
主要的属性如下:

  • position: 其实是指从buffer读取或写入buffer的下一个元素位置。比如,已经写入buffer 3
    个元素那那么position就是指向第4个位置,即position设置为3(数组从0开始计)。
  • limit:还有多少数据需要从buffer中取出,或还有多少空间可以放入。postition总是 <=limit。
  • capacity: 表示buffer本身底层数组的容量。limit绝不能>capacity。

数据结构如下:

  • get()方法,一字节一字节读
  • getChar()、getShort()、getInt()、getFloat()、getLong()、getDouble()读取相应字节数
    的数据

至此:问题显而易见,fastjson在1.2.31及之前,没有提供ByteBuffer 序列化器,所以用了默认的javabean序列化器,而默认的javabean序列化器,又通过get方法反序列化,当遇见ByteBuffer时,ByteBuffer中会先遇到如下方法,getLong(),

    public long getLong() {return Bits.getLong(this, ix(nextGetIndex(8)), bigEndian);}//nextGetIndex final int nextGetIndex(int nb) { // package-private if (limit - position < nb)throw new BufferUnderflowException();int p = position;position += nb;return p;}

每次读取position偏移8个字节,而MessageExt中,构建的ByteBuffer存储的时4个字节,所以会报错,完整的堆栈如下:

上面方法可证明,fastjson序列化是依赖的java方法
getXxx()
boolean isXxx()

redis锁失效

场景:主播通关任务,例如1金币1积分,积分达到2000的时候,开启一个xx活动(比如赠送加速卡,双倍经验时常等),然后积分从0再次开始,依次循环。
实现

    public void rank(long memberId, double score) {String socreKey = memberId + ":"+"score";String lockKey = memberId + "";boolean flag =false;try {flag = lock.lock(lockKey);
//Double newScore = redisUtil.zincrby(socreKey, score, memberId + "");if (newScore >= 2000) {//清0
...
//双倍经验时常 service.add(memberId, score);}}catch (Exception e){//log }finally {if(true){lock.close();}}}

加锁代码:

    @Overridepublic boolean lock(String key) {Jedis jedis = null;try {jedis = jedisPool.getResource();while (true) {String result = jedis.set(key, uuid, "NX", "PX", expireTime);if (LOCK_SUCCESS.equals(result)) {return true;}}return false;} catch (Exception e) {log.error("redis error",e);throw e;} finally {if (jedis != null) {jedis.close();}}}

生产故障:
设计前:加锁考量时,已知存在单点故障,存在释放他人锁可能性,所以简化加了30s,且QPS不太大。
问题点:redis购买的阿里云主从从版本,存在主从延时,造成获取锁的可能性的概率大大增强。
当时活动三天,每天大概锁重复概率事后统计20条左右吧。

分布式锁

Redlock

Redlock 算法介绍

部署多台 Redis, 各实例之间相互独立, 不存在主从复制或者其他集群协调机制
使用方式大体如下:

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock1");
RLock lock3 = redissonInstance3.getLock("lock1");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock1 lock1
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

原理

加入设置节点数N=5,所以我们需要在不同的计算机或虚拟机上运行5个Redis主站,以确保它们会以一种基本独立的方式失败。
为了获得锁,客户端执行以下操作。

  • 获取当前的时间,以毫秒为单位。
  • 依次在所有N个实例中获取锁,在所有实例中使用相同的键名和随机值。在步骤2中,当在每个实例中设置锁时,客户端使用一个与总的锁自动释放时间相比很小的超时来获取它。例如,如果自动释放时间是10秒,超时可以在 5-50毫秒范围内。这可以防止客户端在试图与 Redis节点对话时长时间受阻:如果一个实例不可用,我们应该尽快尝试与下一个实例对话
  • 客户端通过从当前时间减去步骤1中获得的时间戳,计算出获得锁所需的时间。如果并且只有当客户端能够在大多数实例(至少3个)中获取锁,并且获取锁的总时间小于锁的有效期,锁 才被认为是被获取。
  • 如果锁被获取,其有效性时间被认为是初始有效性时间减去经过的时间,如步骤3中计算的那 样。
  • 如果客户端由于某种原因未能获得锁(要么它无法锁定N/2+1个实例,要么有效性时间为负
    数),它将尝试解锁所有的实例(甚至是它认为无法锁定的实例)。

Redlock 算法是否安全

分布式系统研究员Martin Kleppmann曾对 RedLock算法深入分析并强烈反对在生产中使用,其主要原因就是redlock的实现依赖了服务器的本地时钟
如下例子,还是5个节点,Redlock失效:

  1. 客户端1获得了A、B、C节点上的锁,由于网络问题,无法到达D和E。
  2. 节点C上的时钟向前跳动,导致锁过期。
  3. 客户端2获得了节点C、D、E的锁,由于网络问题,A和B不能被联系到。
  4. 客户端1和2现在都认为他们持有锁。
    也或者,在第二步骤,节点c如果出现宕机,恢复后没有之前的数据,客户端2也可能获取到锁

再看如下例子:

  1. 客户端1请求锁定节点A、B、C、D、E。
  2. 当对客户端1的响应在路途中时,客户端1进入停止世界的GC。
  3. 所有Redis节点的锁都过期了。
  4. 客户端2获得了节点A、B、C、D、E的锁。
  5. 客户端1完成了GC,并收到了来自Redis节点的响应,表明它成功获得了锁(当进程暂停
    时,它们被保存在客户端1的内核网络缓冲区)。
  6. 客户端1和2现在都认为他们持有该锁。
    具体可参考:https://www.jianshu.com/p/82b8e603c4ea

double 精准计算

随着经验的增长,你肯定想去深入了解一些常见的东西的细节,浮点数的存储和计算就是这样一种"东西"

  /*** 判断一个坐标点是否包含在当前单元格的范围内 ** @param point 坐标点 * @return 包含在范围内返回true,否则false */public boolean contains(DPoint point) {return (point.x >= startBounds.x&& point.y >= startBounds.y&& (int) (point.x * 1000) < (int) (startBounds.x * 1000) + (int) (getLength() *1000)&& (int) (point.y * 1000) < (int) (startBounds.y * 1000) + (int) (getWidth() *1000));}

以上代码是在已知double计算有误差的时候写的。

这种精度丢失,未采用BigDecimal 是因为性能问题,该方法每秒调用几十万次,有性能问题.
double计算错误案例:

double d2 = 123456.1234567890;
double d3 = 123456.12345678901;
System.out.println(d3-d2);
结果:0
明显是错误的 

double类型在java中的存储结构
类型double大小为8字节,即64位,内存布局如下:
符号位(1 bit) 指数(11 bit) 尾数(52 bit)

符号位 (Sign):0代表正数,1代表为负数;
指数位 (Exponent):用于存储科学计数法中的指数数据;
尾数部分 (Mantissa):采用移位存储尾数部分;

小数用二进制如何表示
首先,给出一个任意实数,整数部分用普通的二进制便可以表示,这里只说小数部分如何表示 例如0.6
文字描述该过程如下:将该数字乘以2,取出整数部分作为二进制表示的第1位;然后再将小数部分乘以2,将得 到的整数部分作为二进制表示的第2位;以此类推,知道小数部分为0。
特殊情况: 小数部分出现循环,无法停止,则用有限的二进制位无法准确表示一个小数,这也是在编程语言中
表示小数会出现误差的原因
下面我们具体计算一下0.6的小数表示过程
0.6 * 2 = 1.2 ——————- 1
0.2 * 2 = 0.4 ——————- 0
0.4 * 2 = 0.8 ——————- 0
0.8 * 2 = 1.6 ——————- 1
0.6 * 2 = 1.2 ——————- 1
…………
我们可以发现在该计算中已经出现了循环,0.6用二进制表示为 1001 1001 1001 1001 ……
如果是10.6,那个10.6的完整二进制表示为 1010.100110011001……
2. 二进制表示的小数如何转换为十进制
其实这个问题很简单,我们再拿0.6的二进制表示举例:1001 1001 1001 1001
文字描述:从左到右,v[i] * 2^( - i ), i 为从左到右的index,v[i]为该位的值:
0.6 = 1 * 2^-1 + 0 * 2^-2 + 0 * 2^-3 + 1 * 2^-4 + ……
0.5
0.0625
0.03125
0.003906
0.001953

验证
案例中:65.383 转换成 二进制存储,结果如下:
1000001.0110001000001100010010011011101001011110001101
转换成科学计数法:
1.0000010110001000001100010010011011101001011110001101 * 10^6
内存布局如下:


上面表格,之所以能用vs去验证,或者说之所以c语言中,double与java一一致,是因为java和c语言,在存储浮点型数据的时候,都采用了 IEEE 754 的标准
IEEE754(美国电器和电子工程师学会)
浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。
指数位采用移码的方式:
移码(又叫增码或偏置码)通常用于表示浮点数的阶码,其表示形式与补码相似,只是其符号位用“1”表示正数,用“0”表示负数,数值部分与补码相同。
所以上面的案例,用于标识6的阶码,是10000000101
011 1111 1111

  • 000 0000 0101
    100 0000 0101
    其中 011 1111 1111=1023,也叫做双精度数的偏差值,对于单精度,该值=127
    为什么用移码?
    便于浮点数比大小。如果阶码(指数)也用补码来表示,就会使得一个浮点数中出现两个符号位:浮点数自身的和浮点数指数部分的。这样的结果是,在比较两个浮点数大小时,无法像比较整数时一样使用简单的无逻辑的二进制比较。这就可能需要重新设计一套电路,不划算。
    另外,通过观察java Double源码,里面有类似:
/**
* Maximum exponent a finite {@code double} variable may have.
* It is equal to the value returned by
* {@code Math.getExponent(Double.MAX_VALUE)}.
*
* @since 1.6
*/
public static final int MAX_EXPONENT = 1023;
/**
* Minimum exponent a normalized {@code double} variable may
* have. It is equal to the value returned by
* {@code Math.getExponent(Double.MIN_NORMAL)}.
*
* @since 1.6
*/
public static final int MIN_EXPONENT = -1022;

的变量,之所以指数位不是2^11次方
double减法运算
7.22-7=0.21999999999999975
之所以这样是因为7.22,无法用一个精确的二进制表示出来

线上故障之-雪花算法重复、序列化、redis锁失效、double精准计算相关推荐

  1. JAVA 线上故障排查指南!

    来源:https://fredal.xin/java-error-check 线上故障主要会包括cpu.磁盘.内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依 ...

  2. 线上故障如何快速排查?来看这套技巧大全

    简介:有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从服务器.Java应用.数据库.Redis.网络和业务六个层面分享线上故障排查的思路和技巧.较长,同学们可收藏后再看. 前言 线 ...

  3. idea本地跑如何看gc日志_线上故障如何快速排查?来看这套技巧大全

    简介:有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从服务器.Java应用.数据库.Redis.网络和业务六个层面分享线上故障排查的思路和技巧.较长,同学们可收藏后再看. 前言 线 ...

  4. du -sh 如何找到最大的文件夹_线上故障如何快速排查?来看这套技巧大全

    简介:有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从服务器.Java应用.数据库.Redis.网络和业务六个层面分享线上故障排查的思路和技巧.较长,同学们可收藏后再看. 前言 线 ...

  5. 阿里技术:如何快速排查线上故障?

    阿里技术:如何快速排查线上故障? 以下文章来源于阿里技术 ,作者小峯 阿里技术 阿里巴巴官方技术号,关于阿里的技术创新均呈现于此. 有哪些常见的线上故障?如何快速定位问题?本文详细总结工作中的经验,从 ...

  6. 【深入理解JVM】JAVA线上故障排查全套路

    线上故障主要会包括cpu.磁盘.内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍.同时例如jstack.jmap等工具也是不囿于一个方面的问题的,基 ...

  7. JAVA 线上故障排查套路,从 CPU、磁盘、内存、网络到GC 一条龙!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 线上故障主要会包括cpu.磁盘.内存以及网络问题,而大多数 ...

  8. JAVA 线上故障排查完整套路,从 CPU、磁盘、内存、网络、GC 一条龙!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:fredal https://fredal.xin/java ...

  9. JAVA 线上故障排查完整套路!牛掰!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源丨8rr.co/kV3R 线上故障主要会包括 CPU.磁盘.内 ...

最新文章

  1. python 倒计时_用Python帮你远离猝死悲剧
  2. shell的最大命令行长度
  3. RHEL在戴尔系统上p1p1 ......命名规则
  4. php基础教程 第七步数组补充及循环基础
  5. Zabbix3.2安装
  6. 企业高可用切换的说明
  7. 埋坑一: vue中子组件调用兄弟组件方法
  8. java.lang.ExceptionInInitializerError解决办法
  9. 【无一时】的意思和解释
  10. 【TSP】基于matlab遗传算法求解13城市旅行商问题【含Matlab源码 1255期】
  11. 机顶盒怎样配置服务器信息,网络机顶盒桌面配置服务器
  12. 小米手机全黑屏9008救砖
  13. 考计算机初级难不难,初级程序员好考吗_考试难不难_上学吧
  14. mapgis编辑属性结构编辑不了_在win 7系统中MAPGIS的区属性结构修改不了,而且出现死机...
  15. [小甲鱼] 零基础入门python第019讲课后测试题及答案:我的地盘听我的
  16. 非单射一致性和单射一致性的概念辨析
  17. 如何强制重启M1 Mac MacBook Pro?
  18. python读取包含层级关系的excel
  19. ctcpejmu单词_微生物英文单词
  20. 【C语言】习题3-2 高速公路超速处罚

热门文章

  1. html怎么添加圆圈按钮,如何使用HTML5和CSS 3在圆圈周围放置按钮?
  2. python社团宣传语_用Python做一个好玩的朋友圈九宫格抽奖
  3. 如果计算机是中国人发明的
  4. 一篇解单链表(0基础看)(C语言)《数据结构与算法》
  5. 案例-抓取网页图片实现高效存图
  6. 数字时代的声音科学:从数学之父读懂华为最大音频实验室
  7. Liquid lens technology: Principle of electrowetting based lenses and applications to imaging
  8. linux建立git本地共享仓库和个人仓库
  9. linux grub.cfg挂了,关于linux系统grub.cfg文件的问题(百度到的内容,记录一下)-Go语言中文社区...
  10. n!末尾有多少个0问题