• java设计实现10位纯数字短id工具类【从浅入深,保姆级】


    相关资料

    如果喜欢看视频详细讲解: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());
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    集成redis

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

    • 导入依赖
    <dependency>
      <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 添加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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    配合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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    完整工具类代码

    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));
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108

    测试代码

    我们写一个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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    数据采集之:巧用布隆过滤器提取数据摘要
    【JVM】字节码文件的组成部分
    计算机网络学习四(网络层概述)
    5个PS学习、练习素材网站
    DataLoader
    云计算与大数据课程笔记(四)之Google云计算框架辅助笔记(下)
    对称加密和非对称加密以及CA证书
    ESWC 2018 | R-GCN:基于图卷积网络的关系数据建模
    干货很干:5个有效引流方法,让客户找上门
    计算机毕业设计(附源码)python-新型冠状病毒防控咨询网站
  • 原文地址:https://blog.csdn.net/qq_42365534/article/details/126454795