項(xiàng)目中使用的是hutool工具類庫(kù)提供的雪花算法生成id方式,版本使用的是5.3.1
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.1</version>
</dependency>
雪花算法生成id方式提供了getSnowflake(workerId,datacenterId)獲取單例的Snowflake對(duì)象,并對(duì)生成id的方法nextId()進(jìn)行了synchronized加鎖處理。
IdUtil
public static Snowflake getSnowflake(long workerId, long datacenterId) {
return Singleton.get(Snowflake.class, workerId, datacenterId);
}
Snowflake
public synchronized long nextId() {
long timestamp = genTime();
if (timestamp < lastTimestamp) {
// 如果服務(wù)器時(shí)間有問(wèn)題(時(shí)鐘后退) 報(bào)錯(cuò)。
throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | (dataCenterId << dataCenterIdShift) | (workerId << workerIdShift) | sequence;
}
項(xiàng)目中使用雪花算法
IdUtils
public class IdUtils {
private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(1, 1);
public static Long getNextId() {
return SNOWFLAKE.nextId();
}
}
舉例controller
UserController
@Slf4j
@RestController
@RequestMapping("/id")
public class UserController {
@Autowired
private IUserService userService;
@GetMapping("/next")
public Long next() {
Long id = IdUtils.getNextId();
User user = new User().setId(id);
boolean save = userService.save(user);
if (save) {
return id;
}
return 0L;
}
}
線上環(huán)境報(bào)例如:BatchUpdateException: Duplicate entry ‘1531683498452185090’ for key ‘PRIMARY’ 插入主鍵沖突問(wèn)題。
分析代碼,定位到雪花算法生成id時(shí)出現(xiàn)了問(wèn)題
首先排除時(shí)鐘回退的情況,因?yàn)樵?.3.1版本如果服務(wù)器時(shí)間有問(wèn)題(時(shí)鐘后退) 直接報(bào)錯(cuò)。
1單機(jī)
排除單機(jī)情況下出現(xiàn)id重復(fù)問(wèn)題,SNOWFLAKE 是單例的,并且生成id的方法被synchronized修飾。
2集群環(huán)境下
需要手動(dòng)設(shè)置dataCenterId 和 workerId值,不同機(jī)器相同時(shí)間戳要想保證生成的id不重復(fù),那么dataCenterId 和workerId的組合必須是唯一的
private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(workerId , dataCenterId );
Mybatis-Plus v3.4.2 雪花算法實(shí)現(xiàn)類 Sequence,提供了兩種構(gòu)造方法:無(wú)參構(gòu)造,自動(dòng)生成 dataCenterId 和 workerId;有參構(gòu)造,創(chuàng)建 Sequence 時(shí)明確指定標(biāo)識(shí)位
Hutool v5.7.9 參照了 Mybatis-Plus dataCenterId 和 workerId 生成方案,提供了默認(rèn)實(shí)現(xiàn)
一起看下 Sequence 的創(chuàng)建默認(rèn)無(wú)參構(gòu)造,如何生成 dataCenterId 和 workerId
public static long getDataCenterId(long maxDatacenterId) {
long id = 1L;
final byte[] mac = NetUtil.getLocalHardwareAddress();
if (null != mac) {
id = ((0x000000FF & (long) mac[mac.length - 2])
| (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
return id;
}
入?yún)?maxDatacenterId 是一個(gè)固定值,代表數(shù)據(jù)中心 ID 最大值,默認(rèn)值 31
為什么最大值要是 31?因?yàn)?5bit 的二進(jìn)制最大是 11111,對(duì)應(yīng)十進(jìn)制數(shù)值 31
獲取 dataCenterId 時(shí)存在兩種情況,一種是網(wǎng)絡(luò)接口為空,默認(rèn)取 1L;另一種不為空,通過(guò) Mac 地址獲取 dataCenterId
可以得知,dataCenterId 的取值與 Mac 地址有關(guān)
接下來(lái)再看看 workerId
public static long getWorkerId(long datacenterId, long maxWorkerId) {
final StringBuilder mpid = new StringBuilder();
mpid.append(datacenterId);
try {
mpid.append(RuntimeUtil.getPid());
} catch (UtilException igonre) {
//ignore
}
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
入?yún)?maxWorkderId 也是一個(gè)固定值,代表工作機(jī)器 ID 最大值,默認(rèn)值 31;datacenterId 取自上述的 getDatacenterId 方法
name 變量值為 PID@IP,所以 name 需要根據(jù) @ 分割并獲取下標(biāo) 0,得到 PID
通過(guò) MAC + PID 的 hashcode 獲取16個(gè)低位,進(jìn)行運(yùn)算,最終得到 workerId
分配標(biāo)識(shí)位
Mybatis-Plus 標(biāo)識(shí)位的獲取依賴 Mac 地址和進(jìn)程 PID,雖然能做到盡量不重復(fù),但仍有小幾率
當(dāng)然了我們也可以自己實(shí)現(xiàn)生成workerId、datacenterId的策略
如下,但并未測(cè)試過(guò)
@Configuration
public class SnowFlakeIdConfig {
@Bean
public SnowFlakeIdUtil propertyConfigurer() {
return new SnowFlakeIdUtil(getWorkId(), getDataCenterId(), 10);
}
/**
* workId使用IP生成
* @return workId
*/
private static Long getWorkId() {
try {
String hostAddress = Inet4Address.getLocalHost().getHostAddress();
int[] ints = StringUtils.toCodePoints(hostAddress);
int sums = 0;
for (int b : ints) {
sums = sums + b;
}
return (long) (sums % 32);
}
catch (UnknownHostException e) {
// 失敗就隨機(jī)
return RandomUtils.nextLong(0, 31);
}
}
/**
* dataCenterId使用hostName生成
* @return dataCenterId
*/
private static Long getDataCenterId() {
try {
String hostName = SystemUtils.getHostName();
int[] ints = StringUtils.toCodePoints(hostName);
int sums = 0;
for (int i: ints) {
sums = sums + i;
}
return (long) (sums % 32);
}
catch (Exception e) {
// 失敗就隨機(jī)
return RandomUtils.nextLong(0, 31);
}
}
}
很顯然這些方法都依賴于獲取ip 等信息,比如ip并非連續(xù),甚至獲取不到ip等信息時(shí),還是有可能出現(xiàn)id重復(fù)問(wèn)題
3docker容器
就比如在docker容器中,一般ip都是隨機(jī)的,并且未經(jīng)過(guò)設(shè)置還無(wú)法獲得ip信息。
docker容器和宿主機(jī)環(huán)境是隔離的,但是可以在啟動(dòng)docker容器時(shí)將宿主機(jī)的主機(jī)名以環(huán)境變量的形式傳入,代碼在容器中獲取該值即可。
這里采用另一種方法,我們可以手動(dòng)設(shè)置workid生成規(guī)則,并存到redis中。
這里只設(shè)置了workId,保證workId和dataCenterId的組合不重復(fù)就可以。
workId的生成是系統(tǒng)每次啟動(dòng),第一次獲取Snowflake 對(duì)象時(shí)才會(huì)進(jìn)行,
public class IdUtils {
private static StringRedisTemplate stringRedisTemplate = ApplicationContextHolder.getBean(StringRedisTemplate.class);
private static String SNOWFLAKE_WORKID = "snowflake:workid";
private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(getWorkerId(SNOWFLAKE_WORKID), 1);
public static Long getNextId() {
return SNOWFLAKE.nextId();
}
/**
* 容器環(huán)境生成workid 并redis緩存
* @param key
* @return
*/
public static Long getWorkerId(String key) {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redis_worker_id.lua")));
redisScript.setResultType(Long.class);
return stringRedisTemplate.execute(redisScript, Collections.singletonList(key));
}
}
ApplicationContext對(duì)象的獲取 ,解決使用注解獲取不到bean的問(wèn)題
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder.applicationContext = applicationContext;
}
/**
* 全局的applicationContext對(duì)象
* @return applicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String beanName) {
return (T) applicationContext.getBean(beanName);
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
lua腳本 redis_worker_id.lua
workId初始為0 ,每次獲取后+1,知道獲取到31后重置為0
為什么上限是31呢,因?yàn)閣orkId默認(rèn)占5bit
local isExist = redis.call('exists', KEYS[1])
if isExist == 1
then
local workerId = redis.call('get', KEYS[1])
workerId = (workerId + 1) % 31
redis.call('set', KEYS[1], workerId)
return workerId
else
redis.call('set', KEYS[1], 0)
return 0
end
測(cè)試
使用nginx 端口8080
location /api {
default_type application/json;
#internal;
keepalive_timeout 30s;
keepalive_requests 1000;
#支持keep-alive
proxy_http_version 1.1;
rewrite /api(/.*) $1 break;
proxy_pass_request_headers on;
#more_clear_input_headers Accept-Encoding;
proxy_next_upstream error timeout;
#proxy_pass http://127.0.0.1:8081;
proxy_pass http://backend;
}
}
upstream backend {
server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;
server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
}
代理訪問(wèn)8081、8082兩個(gè)項(xiàng)目
將自己的項(xiàng)目端口號(hào)設(shè)置為8081,并復(fù)制Copy Configuration ,VM options設(shè)置
-Dserver.port=8082
這樣啟動(dòng)nginx8080,項(xiàng)目8081、8082
然后使用Jmeter進(jìn)行壓測(cè),比如1000個(gè)線程 循環(huán)10次進(jìn)行插入數(shù)據(jù)庫(kù)
訪問(wèn)路徑
不再出現(xiàn)id重復(fù)問(wèn)題
參考
https://blog.csdn.net/weixin_36586120/article/details/118018414文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-422534.html
https://www.cnblogs.com/hzzjj/p/15117771.html
https://blog.csdn.net/nickDaDa/article/details/89357667文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-422534.html
到了這里,關(guān)于線上使用雪花算法生成id重復(fù)問(wèn)題的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!