likes
comments
collection
share

雪花❄️算法ID重复-DB唯一键冲突-复现分析到解决-线上环境

作者站长头像
站长
· 阅读数 26

1. 背景

  • 在某天下午,群里突然出现告警,出现大量短信插入数据失败异常,最后定位到线上环境短信发送记录入库唯一键冲突问题。目前发现问题是由于不同的服务器上雪花算法机器码wordId相同导致。当发送短信功能在高并发请求下,大量请求在同一毫秒下并且workId在两台物理机Java服务上是一样的,就会出现ID重复情况, 下面是具体的设计:

    • workId获取流程【workId存放在Redis的Hash结构里面,key是服务名+ip地址,value是map.size()大小,但是会出现同一个服务在不同机器的wordId相同, 正好碰上时间戳也是一致,并且sequence序列号也是一致的,虽然sequence递增过程加了sync锁,但是jvm层面的锁解决不了多台机器分布式的问题,最终导致雪花算法生成的ID出现一致问题】
  • 现在版本的workerId是拿到最大workerId【map.size()】,再workerId递增。不知道哪个神仙设计的用Redis hash数据结构来获取workId,获取逻辑全是漏洞。

  • 正常流程】user有2个节点,当前map.size()=99,第一个节点启动map.size=100 第二个节点启动是101,这个流程是没问题的。

  • 异常流程】比如user 2个节点,当前map.size()=99,第一个节点启动map.size()=100,mall服务掉了一个节点redis的hash删除这个节点,现在map.size()=99, 当第二个user服务节点启动又是100,两个user的wordId都是100,生成雪花算法bit组成结构(timestamp+wordId+sequence)。

雪花❄️算法ID重复-DB唯一键冲突-复现分析到解决-线上环境

2. 雪花算法

  • 64位 由第一位固定为1位0(正整数),时间戳占41位, 机器wordId占10位,sequence有12位(4096)
  • 1+41+10+12=64位

雪花❄️算法ID重复-DB唯一键冲突-复现分析到解决-线上环境

/**
 * Copyright (c) 2024
 * All rights reserved
 * Author: hakusai22@qq.com
 */

public class SnowflakeIdGenerator {
  private static final long startTime = 1532016000000L;
  private static final long workerIdBits = 10L;
  private static final long maxWorkerId = 1023L;
  private static final long sequenceBits = 12L;
  private static final long workerIdMoveBits = 12L;
  private static final long timestampMoveBits = 22L;
  private static final long sequenceMask = 4095L;
  private long workerId;
  private static long sequence = 0L;
  private static long lastTimestamp = -1L;
  private static SnowflakeIdGenerator idWorker;

  /**
   * synchronized JVM锁 获取雪花ID ❄️.
   *
   * @return
   */
  public synchronized long nextNormalId() {
    long timestamp = this.currentTime();
    if (timestamp < lastTimestamp) {
      throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
    } else {
      if (lastTimestamp == timestamp) {
        sequence = sequence + 1L & 4095L;
        if (sequence == 0L) {
          timestamp = this.blockTillNextMillis(lastTimestamp);
        }
      } else {
        sequence = 0L;
      }

      lastTimestamp = timestamp;
      //64位 由第一位固定为1位0(正整数),时间戳占41位, 机器wordId占10位,sequence有12位(4096)
      return timestamp - 1532016000000L << 22 | this.workerId << 12 | sequence;
    }
  }
}

3. (优化算法后的版本)初始化workId在本地服务中,Springboot容器加载完调用

  • 查找workerId是否有空洞,有则填上(复用前面的值);没有则取当前最大数+1。
    • 先拿到map.size()大小,使用一个set容器存储所有的value值,再拿到map中value最大的值,从最大值-1开始往前去set容器中查找不存在的值, 这个值就是workId,如果前面的值都用完了,再从最大的value递增+1生成新的workId。

雪花❄️算法ID重复-DB唯一键冲突-复现分析到解决-线上环境

Springboot 启动 初始化workId在本地服务中

  • 启动的时候设置一个redis分布式锁🔒,超时时间是9s,这过程保证多个服务同时请求抢锁获取workId逻辑 不会出现workId一致的情况。为什么设置的是9s呢?9s内没有处理完锁被释放了,别的服务获取到锁 还需要处理第一个服务防止把后面的锁给误删了的逻辑(使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁)。

/**
 * Copyright (c) 2024
 * All rights reserved
 * Author: hakusai22@qq.com
 */

/**
 * spring 容器加载完毕后调用
 */
@Component
@Slf4j
public class ApplicationInit implements ApplicationContextAware {

  @Autowired
  private RedisUtil redisUtil;

  @Value("${spring.application.name}")
  private String name;

  /**
   * 1.初始化SnowflakeIdGenerator
   */
  @Override
  public void setApplicationContext(
      ApplicationContext applicationContext) throws BeansException {
    String snowflakeId = "SnowflakeId";
    try {
      // 分布式锁🔒
      redisUtil.lock(snowflakeId + "-key", 90000L, () -> {
        Map<String, Object> map = redisUtil.hmget(snowflakeId);
        long workId = 0;
        if (map == null || map.size() == 0) {
          map = new HashMap<>(16);
          map.put(name + "-" + getIpAddress(), workId);
        } else {
          Object o = map.get(name + "-" + getIpAddress());
          if (o != null) {
            workId = Long.parseLong(o.toString());
          } else {
            workId = getNextWorkerId(map);
            map.put(name + "-" + getIpAddress(), workId);
          }
        }
        SnowflakeIdGenerator.init(workId);
        redisUtil.hmset(snowflakeId, map);
        return null;
      });
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * 查找workerId是否有空洞,有则填上;没有则取当前最大数+1
   *
   * @param map
   * @return
   */
  private static long getNextWorkerId(Map<String, Object> map) {
    Long nextWorkerId = -1L;
    Set<Long> currentWorkerIdSet = new HashSet<>(map.size());
    for (Map.Entry<String, Object> entry : map.entrySet()) {
      Long value = Long.parseLong(entry.getValue().toString());
      nextWorkerId = nextWorkerId.compareTo(value) > 0 ? nextWorkerId : value;
      currentWorkerIdSet.add(value);
    }
    long temp = nextWorkerId;
    while (--temp >= 0) {
      if (!currentWorkerIdSet.contains(temp)) {
        return temp;
      }
    }
    return ++nextWorkerId;
  }

4. 总结

  • 业务中出现雪花❄️ID重复主要是:
    1. workId算法生成设计有问题,优化生成workId算法就好了。
    2. 业务并发量高,同一个时间戳需要生成的ID调用很多,同一个服务synchronized锁只能解决本机重复问题,服务多节点部署就会出现问题,在workId相同情况下,添加redis分布式锁解决时间戳毫秒一致现象。
  • 收获的话: 主要是排查问题的过程,通过日志和源代码进行分析,代码复习场景,文档总结描述,解决代码上的问题,还能学习一波雪花ID相关知识点。

5. 参考

转载自:https://juejin.cn/post/7376853374250123314
评论
请登录