上一篇中我们介绍了《为什么亿级数据量时要使用位图》,今天这一篇就来实战使用位图。
做分布式项目首先要考虑分布式ID的生成,分布式ID暂时没有太好的高性能全局递增或趋势递增,即使是美团的lefa、百度的uid-generator、滴滴的tinyid都或多或少借用了第三方中间件不但对性能有所影响也增加了系统的复杂度。
我在之前的文章《 高并发下分布式ID各个的解决方案以及redis集群分布式ID代码实现》介绍了使用Redis生成,但是这些ID都有一个共同点,要么只是递增 例如 1、2、3…1000,规律太过明显,要么像雪花算法一样生成的过长,既增加了系统的复杂度也不利于展示给用户查看。
那么有没有一直方案能够高性能的生成分布式ID,且没有过于明显的规律,且生成的长度完全由自己掌握并且用户能够感觉到自己目前的账号所处的位置呢?
我们这里使用了位图BitMap,完全基于内存实现效率很高,平均1秒生成20万个ID,如果用到了redis那么生成的效率与redis支持的最高写操作齐平,1s/8w。
因为我们使用的这个位图在Maven开源仓库中没有,所以这里我们只能手动上传到本地,也可以上传到我们自己服务器的远程私服中。
jar包下载:CSDN下载资源
进入到jar包目录
cd d:
cd sa-jdi
mvn install:install-file -DgroupId=sun.jvm.hotspot -DartifactId=sa-jdi -Dversion=1.8.0 -Dpackaging=jar -Dfile=sa-jdi-1.8.0.jar
maven引入
<dependency>
<groupId>sun.jvm.hotspotgroupId>
<artifactId>sa-jdiartifactId>
<version>1.8.0version>
dependency>
import org.apache.commons.lang3.RandomUtils;
import sun.jvm.hotspot.utilities.BitMap;
import java.util.ArrayList;
import java.util.List;
/**
* @Author liuy
* @Description 会员号生成器
* @Version 1.0
*/
public class Test {
public static void main(String[] args) {
//开始时间戳
Long startTime = System.currentTimeMillis();
List<Long> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
Long id = generateId();
System.out.println(id);
list.add(id);
}
System.out.println("生成id一共 : " + list.size());
//结束的时间戳
Long endTime = System.currentTimeMillis();
//所耗时间
System.out.println("花费时间" + (endTime - startTime) + "ms");
}
// base是以1000为单位,bitmap偏移
private static Integer base = 1;
//代表每个偏移量只生成4000个号码 例如 偏移量为1 那么 之后生成1001 - 4000 偏移量为4 生成范围:4001-7000
private static Integer batchSize = 4000;
// 一个号段4000
static BitMap bitMap = new BitMap(batchSize);
static {
loadFromDB();
}
/**
* 刷新数据库或redis 动态生成偏移量
*/
private static void loadFromDB() {
// query DB 生成一批 / 1000 为 base
//base 偏移量 每次增加三 能保证如果这次服务中断 那么下次生成的id不会是之前生成过的id
base += 2;
bitMap.clear();
}
public static Long generateId() {
int offset = RandomUtils.nextInt(0, batchSize);
// 已被使用,类似
while (offset < batchSize && bitMap.at(offset)) {
// 下一个未使用的bit
offset++;
}
if (offset == batchSize) {
loadFromDB();
return generateId();
}
bitMap.atPut(offset, true);
//这里号段内的随机数 * 3 是为了打乱 可以自定义
return (long) (base * 10000 + (offset * 3) );
}
}
性能极高。
1.完全基于内存也就是平常说的用空间换时间,如果服务器配置不高有可能会引起服务崩溃。
2.这里只使用了本地缓存,如果是分布式部署的话,那么偏移量将会重置,有可能造成ID重复的情况。
把偏移量保存到redis 每次服务重启都要增加一次偏移量 保证不重复。
import com.soboot.common.core.utils.SoBootLogger;
import org.apache.commons.lang3.RandomUtils;
import org.junit.Test;
import org.springframework.context.support.ApplicationObjectSupport;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import sun.jvm.hotspot.utilities.BitMap;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @Author liuy
* @Description 会员号生成器
* @Version 1.0
*/
@Component
public class IdTools extends ApplicationObjectSupport {
private static RedisTemplate<String, Object> redisTemplate;
@Test
public void test() {
//开始时间戳
Long startTime = System.currentTimeMillis();
List<Long> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
Long id = generateId();
System.out.println(id);
list.add(id);
}
System.out.println("生成id一共 : " + list.size());
//结束的时间戳
Long endTime = System.currentTimeMillis();
//所耗时间
System.out.println("花费时间" + (endTime - startTime) + "ms");
}
// base是以1000为单位,bitmap偏移
private static Integer base = 4;
//代表每个偏移量只生成4000个号码 例如 偏移量为1 那么 之后生成1001 - 4000 偏移量为4 生成范围:4001-7000
private static Integer batchSize = 1000;
// 一个号段4000
static BitMap bitMap = new BitMap(batchSize);
@PostConstruct
public void init() {
try {
redisTemplate = (RedisTemplate<String, Object>) getApplicationContext().getBean("redisTemplate");
if (null == redisTemplate) {
SoBootLogger.warn("缺少Redis相关配置,Id号段式生成器 无法使用");
}
} catch (Exception e) {
SoBootLogger.error("获取Id号段式生成器配置错误,Id生成器无法使用", e);
}
loadFromDB();
}
/**
* 刷新数据库或redis 动态生成偏移量 把偏移量保存到redis 每次服务重启都要增加一次偏移量 保证不重复
*/
private static void loadFromDB() {
ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
Object version = opsForValue.get("SOBOOT_ID_GENERATE_OFFSET_BASE");
int offset = 0;
if (!Objects.isNull(version)) {
offset = (Integer) version;
}
// query DB 生成一批 / 1000 为 base
//base 偏移量 每次增加三 能保证如果这次服务中断 那么下次生成的id不会是之前生成过的id
//3 代表步数 也可以自定义设置步数
base = offset + 2;
opsForValue.set("SOBOOT_ID_GENERATE_OFFSET_BASE", base);
bitMap.clear();
}
public static Long generateId() {
int offset = RandomUtils.nextInt(0, batchSize);
// 已被使用,类似
while (offset < batchSize && bitMap.at(offset)) {
// 下一个未使用的bit
offset++;
}
if (offset == batchSize) {
loadFromDB();
return generateId();
}
bitMap.atPut(offset, true);
//这里号段内的随机数 可以自定义
return (long) (base * 10000 + (offset * 3) );
}
}
分布式部署时不会造成ID重复的情况,每次重启服务或部署新服务时会根据redis中的偏移量进行号段的增长,可以保证生成的ID是呈趋势递增的情况。
引入了redis,增加了系统的复杂度。
若多次生成的offset和batchSize相同那么会浪费掉很多号段,会员号的长度也会大幅度增加。
后续优化方案需要避免浪费和避免会员号的长度大幅度增加。