文章目录
- 相关资料
- 引入
- 思路整理
- 设计
- 逐步实现
- 短id生成算法
- 集成redis
- 配合redis完成代码
- 完整工具类代码
- 测试代码
相关资料
如果喜欢看视频详细讲解: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次