相关资料

如果喜欢看视频详细讲解:b站编程小龙

项目代码地址:gitee地址

引入

三四十位的UUID和18位的的雪花ID在库中做业务完全没问题,但是如果放在前端展示用来搜索用户,那展示效果就一言难尽…,你能想象如果自己的QQ账号有近20位是什么个场景?

思路整理

10位数字的极限是999999999,也就是可以表示100亿-1个数值,用作用户的唯一标识完全够了,【哪个系统会有100亿个用户?】,于是乎,我们很容易想到,直接使用随机数遍历生成每一位即可。

问一: 那么问题来了,我们应该怎么保证数字不重复呢?

  • 程序里面用一个set集合缓存就好了,重复就就重试呗

问二: 程序重启了怎么办?

  • 那就存在redis缓存里呗

问三: 那如果我有1000万个用户,就得存1000万个id,redis会爆炸吗?

  • 额,(⊙o⊙)… 对对对对对对…
    在这里插入图片描述

设计

如果所有的短id全部冗余存储到redis中,肯定是有大问题的,如果这个应用是个爆款,用户量激增,用不了多久就会把redis撑爆,那么这个问题该怎么解决呢?

其实很简单,我们不随机所有位,固定前几位按一定规律在一定时间后进行变化,后几位走随机,那么我们就只需要维护这个时间段内的id不重复即可,当前几位变化后,就可以清除一次缓存,这样缓存就不会很膨胀,例如:

  • 1-2位我们取当前年份的后两位,如2022年 =>22
  • 3-5位我们取今天是一年中的第几天,如2022年8月21日 => 223
  • 后五位我们走随机数进行随机,相当于每天可以有10万-1个新增id
    也就是说我们可以每天清除一次缓存,而且每天的年月都不一样,可以保证100年内不重复【前提是当天的id增量小于10万】

在这里插入图片描述

逐步实现

短id生成算法

对照我们的生成策略,我们可以直接使用 时间类对象,非常方便的获取年份和一年中的第几天,再用Math中的random方法写一个简单随机数方法:

  • 需要注意的是,天数小于100时需自行补0占位
private Long generateShortId() {
        // 2 位 年份的后两位 22001 后五位走随机  每天清一次缓存 99999 10
        StringBuilder idSb = new StringBuilder();
        /// 年份后两位  和  一年中的第几天
        LocalDate now = LocalDate.now();
        String year = now.getYear() + "";
        year = year.substring(2);
        String day = now.getDayOfYear() + "";
        /// 补0
        if (day.length() < 3) {
            StringBuilder sb = new StringBuilder();
            for (int i = day.length(); i < 3; i++) {
                sb.append("0");
            }
            day = sb.append(day).toString();
        }
        idSb.append(year).append(day);
        /// 后五位补随机数
        for (int i = idSb.length(); i < 10; i++) {
            idSb.append((int) (Math.random() * 10));
        }
        return Long.parseLong(idSb.toString());
    }

集成redis

这里直接使用springboot-data-redis操作redis

  • 导入依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 添加redis配置类
/**
 * redis 配置类
 */
@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory factory;

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
}

配合redis完成代码

 @Autowired
    private SnowflakeGenerator snowflakeGenerator;
    @Autowired
    private SetOperations<String, Object> setOperations;

    private static String SHORT_ID_SET_KEY = "short_id_set"; // redis缓存中的key
    private static int max_retry_count = 10;//设置最大重试次数

    public Long generate() {
        Long shortId = null;
        boolean exists = true;
        int count = 0;
        while (exists) {
            if (count > max_retry_count) {
                log.error("尝试生成短id发生碰撞超过10次");
                return null;
            }
            shortId = generateShortId();
            exists = setOperations.add(SHORT_ID_SET_KEY, shortId.toString()) == 0;
            count++;
        }
        return shortId;
    }

完整工具类代码

package online.longzipeng.mywebdemo.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.stereotype.Component;

import java.time.LocalDate;

/**
 * @Author: lzp
 * @description: 短id工具类
 * 注意: 使用此工具类,需要自行配置定时任务每日删除该缓存key,具体策略看生成方法的注解
 * @Date: 2022/8/15
 */
@Component
@Slf4j
public class ShortIdUtils {

    /**
     * 短id 缓存key
     */
    public static final String SHORT_ID_SET_KEY = "short_id_set";

    /**
     * 最大重试次数
     */
    public static final int MAX_RETRY_COUNT = 10;

    /**
     * 最大重试记录
     */
    public static int maxCount = 0;
    /**
     * 碰撞次数
     */
    public static int collisionCount = 0;

    @Autowired
    private SetOperations<String, Object> setOperations;

    /**
     * 生成规则 年份后两位【2位】 + 1年内第几天【3位】 + 随机数【5位】
     * 理论上来说,一百年内不会重复
     * 注意:1.前五位保证每天生成的id不重复,后五位会在redis缓存 set集合中进行碰撞校验,
     * 2.请保证每天0时清除缓存,保证缓存中的短id数量较少提高性能,减少内存浪费
     */
    public Long generate() {
        Long id = null;
        int count = 0;
        boolean exists = true;
        // 如果redis中已有该id,重试生成短id
        while (exists) {
        		 // 一下两个仅用于测试碰撞情况,生产环境请注释掉,并且只适用于单线程测试
            if (count > collisionCount) {
                collisionCount++;
            }
            if (count > maxCount) {
                maxCount = count;
            }
            
            id = this.generateShortId();
            exists = setOperations.add(SHORT_ID_SET_KEY, id.toString()) == 0;
            count++;
            if (count >= MAX_RETRY_COUNT) {
                log.error("尝试生成短id发生碰撞超过10次!!");
                return null;
            }
        }
        return id;
    }

    /**
     * 生成短id
     */
    private Long generateShortId() {
        LocalDate now = LocalDate.now();
        String yearStr = now.getYear() + "";
        yearStr = yearStr.substring(2);
        String dayOfYearStr = now.getDayOfYear() + "";
        if (dayOfYearStr.length() < 3) {
            StringBuilder zeroStr = new StringBuilder();
            for (int i = dayOfYearStr.length(); i < 3; i++) {
                zeroStr.append("0");
            }
            dayOfYearStr = zeroStr + dayOfYearStr;
        }
        StringBuilder id = new StringBuilder(yearStr + dayOfYearStr);
        for (int i = id.length(); i < 10; i++) {
            id.append(getRandomIndex(0, 9));
        }
        return Long.parseLong(id.toString());
    }


    /**
     * 返回范围内的随机int
     *
     * @param min 最小值
     * @param max 最大值
     * @return
     */
    private int getRandomIndex(int min, int max) {
        return min + (int) (Math.random() * (max - min + 1));
    }

}

测试代码

我们写一个springboot的单元测试:

package online.longzipeng.mywebdemo;

import online.longzipeng.mywebdemo.utils.ShortIdUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MyWebDemoApplicationTests {

    @Autowired
    private ShortIdUtils shortIdUtils;

    @Test
    public void testShortId() {
        for (int i = 0; i < 10000; i++) {
            shortIdUtils.generate();
        }
        System.out.println("最大碰撞次数为=>" + ShortIdUtils.maxCount);
        System.out.println("总碰撞次数=>" + ShortIdUtils.collisionCount);
    }

}

我们生成1万个短id,测试碰撞次数为564次,最多重试次数是3次

在这里插入图片描述

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐