Spring Security 5.7.11 集 Spring Session之 redis 客户端不支持 keyspace-notifications 键空间通知时的过期rediskey清理解决方案

Spring Security 5.7.11 集成Spring Session之 redis 客户端不支持键空间通知时的过期rediskey清理解决方案
0x01 案发现场
由于封装的redis代理客户端不支持键空间通知,因此需要禁用此功能,否则就会报错。
/***
* maxInactiveIntervalInSeconds 设置redis key 的超时时间
* redisNamespace 设置redis 的namespace
* redisFlushMode 刷新策略
* @EnableAsync(mode = AdviceMode.PROXY) 使用JDK动态代理
* @author qingfeng.zhao
* @date 2024/1/13
* @apiNote Redis Session 自定义配置
*/
@EnableRedisHttpSession(
maxInactiveIntervalInSeconds = 2000,
redisNamespace = "${spring.session.redis.namespace}",
cleanupCron = "0 0 3 * * ?",
flushMode = FlushMode.IMMEDIATE,
saveMode = SaveMode.ALWAYS
)
@Configuration
public class MySessionRegistryConfig {
/**
* Redis容器化客户端不支持connection.getConfig命令,因此需要禁用此策略
* @return
*/
@Bean
public ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
}
1.1 问题分析
通过上面代码虽然可以禁用键空间通知,但是由于spring security +spring session清理过期session redis键是依赖于键空间通知的,会导致过期的session无法及时清理,登录接口就会越来越慢,最终拖垮系统。
分析依据如下:
本地debug,看源码梳理执行流程,发现在登录成功处理器,放入session后,会调用ConcurrentSessionControlAuthenticationStrategy的onAuthentication 方法,
里面有这么一段遍历session的逻辑:
ConcurrentSessionControlAuthenticationStrategy的onAuthentication 方法
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
int sessionCount = sessions.size();
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}
if (allowedSessions == -1) {
// We permit unlimited logins
return;
}
if (sessionCount == allowedSessions) {
HttpSession session = request.getSession(false);
if (session != null) {
// Only permit it though if this request is associated with one of the already registered sessions
for (SessionInformation si : sessions) {
if (si.getSessionId().equals(session.getId())) {
return;
}
}
}
// If the session is null, a new one will be created by the parent class, exceeding the allowed number
}
allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
}
getAllSessions方法
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
Collection<ExpiringSession> sessions =
this.sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, name(principal)).values();
List<SessionInformation> infos = new ArrayList<SessionInformation>();
for (ExpiringSession session : sessions) {
if (includeExpiredSessions || !Boolean.TRUE.equals(session.getAttribute(SpringSessionBackedSessionInformation.EXPIRED_ATTR))) {
infos.add(new SpringSessionBackedSessionInformation(session, this.sessionRepository));
}
}
return infos;
}
根据分析这段代码
Collection<ExpiringSession> sessions =
this.sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, name(principal)).values();
分析查询redis key的构造格式
spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount
执行查看这个redis key结果如下:

查看了下testAccount 的登录session信息发现高达2210条记录,难道session 过期时间设置失效导致的?
但是继续ttl 命令发现这个redis key 并没有设置过期时间
ttl spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount

ttl返回结果含义: 正整数:表示键的剩余生存时间(以秒为单位)。- 0:表示键已经过期。
- -1:表示键存在但没有设置过期时间。
- -2:表示键不存在
查阅资料,发现Spring Security以及Spring Session的处理策略:
- spring Session 使用三个 Redis 键来存储每个会话的数据
- 这个键存储会话的实际数据,包括所有会话属性。键名格式如下:
spring:session:${redisNamespace}:sessions:<sessionId>
- 这个键用于存储会话的过期时间,便于 Redis 定期清理过期会话
spring:session:${redisNamespace}:sessions:expires:<expireTimestamp>:<sessionId>
- 这个键用于存储会话的索引,例如,按用户ID进行索引,便于通过索引快速查找会话
spring:session:${redisNamespace}:index:<indexName>:<indexValue>expirations 键是一个时间戳索引,用于跟踪所有即将过期的会话。Spring Session 会为每个会话的过期时间创建一个相应的
expirations 键,通过这些键,可以快速找到即将过期的会话并进行清理。spring:session::expirations:<expireTimestamp>
键空间通知清理相关说明:
首先检查 customizedNotifyOptions 是否包含 "A"(所有事件)。
如果没有 "A",则分别检查是否包含 "g"(全局事件)和 "x"(过期事件),
如果都没有,则分别添加 "g" 和 "x"。
K: Keyspace events, published with keyspace@<db> prefix.
E: Expired events (过期事件), published when a key expires.
g: Generic commands (like DEL, EXPIRE, RENAME) (通用命令事件).
x: Expired events as keyspace events.
A: Alias for all event types (所有事件的别名).
即使会话的最大不活动间隔时间(sessionExpireInSeconds)已经过期,系统仍然会给这些会话一个额外的五分钟,
以处理延迟或确保系统有时间清理和处理这些过期会话。
这种策略可以在保证会话及时过期的同时,为系统管理和清理会话提供一定的缓冲时间。
- 键空间通知官方文档说明: keyspace-notifications
1.2 手动临时解决方案
假如服务名称是my-test-service, 环境是test, 清理账号是testAccount
spring.session.redis.namespace的配置如果是:my-test-service:test:loginUserTokenRedisKey
则登录redis服务器可手动执行排查命令如下:
# 通过适当添加前缀,得到本次会话的哈希键
# 1. 会话数据键 会自动过期 这个键存储会话的实际数据,包括所有会话属性
# test环境
# 判断 redis key类型
type spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:86d91446-c64d-4b3c-904d-ae2a70bb9f81
# 检查session过期时间 正整数:表示键的剩余生存时间(以秒为单位)。 0:表示键已经过期。 -1:表示键存在但没有设置过期时间。 -2:表示键不存在
ttl spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:c9fce88e-33c1-4038-b5d2-77894432dce0
# 获取session详情
hgetall spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:86d91446-c64d-4b3c-904d-ae2a70bb9f81
# 修改session过期时间
expire spring:session:my-test-serivce:test:loginUserTokenRedisKey:sessions:c9fce88e-33c1-4038-b5d2-77894432dce0 10
# 2. 会话过期时间 几分钟过期 精确到分钟
# 测试环境
type spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:expires:1718884803141:e11514b5-5f21-434c-b84f-9ee81045fd72
ttl spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:expires:时间戳:e11514b5-5f21-434c-b84f-9ee81045fd72
type spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:expires:95552a86-0b74-4525-9f1c-bcff46d1e2c7
# 3. 这个键用于存储会话的索引,例如,按用户ID进行索引,便于通过索引快速查找会话
# 查看redis key 值的类型
type spring:session:my-est-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount
# 查看redis key 过期时间设置
ttl spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount
# 查看某账号下的所有session
SMEMBERS spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount
# 清理某账号下的所有session
del spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount
# 4. expirations 键是一个时间戳索引,用于跟踪所有即将过期的会话
# 查看redis key 值的类型
type spring:session:my-test-service:test:loginUserTokenRedisKey:expirations:1718888940000
# 5. 检查过期的session
# 检查session 是否过期
type "spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:expires:e11514b5-5f21-434c-b84f-9ee81045fd72"
ttl "spring:session:my-test-service:test:loginUserTokenRedisKey:sessions:expires:e11514b5-5f21-434c-b84f-9ee81045fd72"
# 查看快过期的session
SMEMBERS spring:session:my-test-service:test:loginUserTokenRedisKey:expirations:1718888940000
# 扫描 控制台不支持,程序支持但是性能太差
scan 0 MATCH spring:session:my-test-service:test:loginUserTokenRedisKey:expirations:{1900544}* COUNT 10
# 5. 通知事件
# redis 容器化不支持
type spring:session:my-test-service:test:loginUserTokenRedisKey:event:created:*
type spring:session:my-test-service:test:loginUserTokenRedisKey:event:created:154f9d8f-13e2-4480-bb9d-af82a772125e
整理出临时解决方案如下:
- 查找key存储的value类型(判断 redis key类型)
type "spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount"
- 查询set key对应的value
SMEMBERS "spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:jfxitong"
清理key 执行命令如下:
del "spring:session:my-test-service:test:loginUserTokenRedisKey:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:testAccount"
其中:
spring:session是默认的Redis key前缀my-test-service:test,其中my-test-service是当前的服务名称,test是部署环境,是自定义的namespace
用来隔离其他服务的,也可以不设置test,这里设置是为了业务隔离环境,方便test和预发环境复用同一个测试redis库。index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
是固定的框架定义的keytestAccount: 绑定的系统账号
1.3 彻底解决方案
这里封装三个接口:
第一个接口可以通过定时任务定时清理过期的redis key,如果只需要定时任务清理,那么直接看第一个接口的实现即可。
第二个和第三个接口是做这个手动处理的功能使用的。
第二个接口根据账号查询用户session信息,方便通过界面手动查看当前账号所有的session信息,见查看所有session
第三个接口,统计当前用户一共有多少个活跃有效的session. 见上面的登录session总个数。
public interface LoginSessionManageService {
/**
* 根据账号清理过期session
* @param cleanSessionAccount
* @return
*/
VueElementAdminResponse fixedCleanExpiredSessionByAccount(String cleanSessionAccount,String fixedSolutionType);
/**
* 根据账号查找用户Session信息
* @param account
* @return
*/
VueElementAdminResponse findSessionInformationByAccount(String account);
/**
* 判断Session 总个数
* @param account
* @return
*/
VueElementAdminResponse findSessionCountByAccount(String account);
}
接口实现如下:
@Slf4j
@Service(value = "loginSessionManageServiceImpl")
public class LoginSessionManageServiceImpl implements LoginSessionManageService {
private static final String DEFAULT_SPRING_SESSION_REDIS_PREFIX = "spring:session:";
private static final String REDIS_NAME_SPACE= SpringBootPropertyUtils.getPropertyValue("spring.session.redis.namespace");
private static final String SESSION_DATA_KEY=":index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:";
@Qualifier(value = "sessionRepository")
@Autowired
RedisIndexedSessionRepository sessionRepository;
@Override
public VueElementAdminResponse fixedCleanExpiredSessionByAccount(String cleanSessionAccount, String fixedSolutionType) {
VueElementAdminResponse vueElementAdminResponse=new VueElementAdminResponse();
Stopwatch stopwatch=Stopwatch.createStarted();
String traceId=vueElementAdminResponse.getSwiftNumber();
try {
String principalKey= DEFAULT_SPRING_SESSION_REDIS_PREFIX +REDIS_NAME_SPACE +SESSION_DATA_KEY+cleanSessionAccount;
log.info("traceId:{},查询{}用户下的所有session列表Redis Key:{}",traceId,cleanSessionAccount,principalKey);
// 查找该用户所有 session
Set<Object> allSessionId = this.sessionRepository.getSessionRedisOperations().boundSetOps(principalKey).members();
switch (fixedSolutionType){
case "1":
// 检测是否会话数据键是否过期后删除
cleanInvalidSessionSolutionOne(vueElementAdminResponse,allSessionId,principalKey,cleanSessionAccount);
break;
case "2":
// 检测 ttl 过期后删除
cleanInvalidSessionSolutionTwo(vueElementAdminResponse,allSessionId,principalKey,cleanSessionAccount);
break;
default:
log.warn("traceId:{},字典表配置过期session清理用户参数错误,不支持的解决方案类型!",traceId);
vueElementAdminResponse.setCode(40001);
vueElementAdminResponse.setMessage("字典表配置过期session清理用户参数错误,不支持的解决方案类型");
vueElementAdminResponse.setData(null);
break;
}
} catch (Exception e) {
log.error("traceId:{},清理用户过期session异常,清理账号:{},清理方案:{},异常详情:"",traceId,cleanSessionAccount,fixedSolutionType,e);
vueElementAdminResponse.setCode(50000);
vueElementAdminResponse.setMessage("清理用户过期session异常");
vueElementAdminResponse.setData(null);
}finally {
stopwatch.stop();
log.info("traceId:{},清理{}用户过期session执行耗时:{}",traceId,cleanSessionAccount,stopwatch);
}
return vueElementAdminResponse;
}
@Override
public VueElementAdminResponse findSessionInformationByAccount(String account) {
VueElementAdminResponse vueElementAdminResponse=new VueElementAdminResponse();
Stopwatch stopwatch=Stopwatch.createStarted();
String traceId=vueElementAdminResponse.getSwiftNumber();
try {
String principalKey= DEFAULT_SPRING_SESSION_REDIS_PREFIX +REDIS_NAME_SPACE +SESSION_DATA_KEY+account;
log.info("traceId:{},查询{}用户下的所有session列表Redis Key:{}",traceId,account,principalKey);
// 查找该用户所有 session
Set<Object> allSessionId = this.sessionRepository.getSessionRedisOperations().boundSetOps(principalKey).members();
Map<String,String> allSessionIdExpiredInfoMap=new HashMap<>();
for (Object currentSessionId: allSessionId){
String currentSessionIdStr=(String)currentSessionId;
// 过期键 会自动过期
String expireKey = DEFAULT_SPRING_SESSION_REDIS_PREFIX+REDIS_NAME_SPACE+":sessions:expires:" + currentSessionIdStr;
Long expiration = this.sessionRepository.getSessionRedisOperations().getExpire(expireKey, TimeUnit.SECONDS);
if(expiration==-2){
allSessionIdExpiredInfoMap.put(currentSessionIdStr,"不存在");
}else if(expiration==-1){
allSessionIdExpiredInfoMap.put(currentSessionIdStr,"未设置过期时间");
}else if(expiration==0){
allSessionIdExpiredInfoMap.put(currentSessionIdStr,"已过期");
}else{
allSessionIdExpiredInfoMap.put(currentSessionIdStr,"还有"+expiration+"秒后过期");
}
}
Map<String,Object> resultMap=new HashMap<>();
// session Id 总个数
resultMap.put("sessionIdTotalCount",allSessionId.size());
// session 过期信息Map
resultMap.put("allSessionIdExpiredInfoMap",allSessionIdExpiredInfoMap);
vueElementAdminResponse.setCode(ResponseCodeEnum.OK.getCode());
vueElementAdminResponse.setMessage("查询用户的session信息成功");
vueElementAdminResponse.setData(resultMap);
} catch (Exception e) {
log.error("traceId:{},查找用户{}的Session信息异常,异常详情:",traceId,account,e);
vueElementAdminResponse.setCode(50000);
vueElementAdminResponse.setMessage("查找用户"+account+"的Session信息异常");
vueElementAdminResponse.setData(null);
} finally {
stopwatch.stop();
log.info("traceId:{},查找{}用户的Session信息执行耗时:{}",traceId,account,stopwatch);
}
return vueElementAdminResponse;
}
private VueElementAdminResponse cleanInvalidSessionSolutionOne(
VueElementAdminResponse vueElementAdminResponse,
Set<Object> allSessionIdSetList,
String principalKey,
String cleanSessionAccount){
Stopwatch stopwatch=Stopwatch.createStarted();
String traceId= vueElementAdminResponse.getSwiftNumber();
try {
Set<String> validSessionIdSetList=new ConcurrentHashSet<>();
// 查找该用户未过期的 Session
Map<String,?> tempMap=sessionRepository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,
cleanSessionAccount);
for (String key : tempMap.keySet()) {
validSessionIdSetList.add(key);
}
log.info("traceId:{},检测到{}用户当前会话数据键中有{}个session,检测到未过期的SessionId有{}个",
traceId,
cleanSessionAccount,
allSessionIdSetList.size(),
tempMap.size());
for (Object sessionId : allSessionIdSetList) {
String checkSessionId= (String)sessionId;
if(!validSessionIdSetList.contains(checkSessionId)){
// 会话数据键 会自动过期
String sessionKey = DEFAULT_SPRING_SESSION_REDIS_PREFIX+REDIS_NAME_SPACE+":sessions:" + checkSessionId;
// 过期键 会自动过期
String expireKey = DEFAULT_SPRING_SESSION_REDIS_PREFIX+REDIS_NAME_SPACE+":sessions:expires:" + checkSessionId;
// 删除会话数据键
this.sessionRepository.getSessionRedisOperations().delete(sessionKey);
log.info("traceId:{},删除{}用户过期的session数据redis key:{}",traceId,cleanSessionAccount,sessionKey);
// 删除过期键
this.sessionRepository.getSessionRedisOperations().delete(expireKey);
log.info("traceId:{},删除{}用户过期的session过期redis key:{}",traceId,cleanSessionAccount,expireKey);
// 删除索引键
this.sessionRepository.getSessionRedisOperations().boundSetOps(principalKey).remove(checkSessionId);
log.info("traceId:{},删除{}用户下redis Key:{}里面的sessionId:{}成功",
traceId,cleanSessionAccount,principalKey,checkSessionId);
}
}
vueElementAdminResponse.setCode(ResponseCodeEnum.OK.getCode());
vueElementAdminResponse.setMessage("解决方案一:定时清理用户"+cleanSessionAccount+"的过期Session成功");
vueElementAdminResponse.setData(null);
} catch (Exception e) {
log.error("解决方案一:定时清理用户:{}的过期Session异常,异常详情:",cleanSessionAccount,e);
vueElementAdminResponse.setCode(50000);
vueElementAdminResponse.setMessage("解决方案一:定时清理用户"+cleanSessionAccount+"的过期Session异常");
vueElementAdminResponse.setData(null);
} finally {
stopwatch.stop();
log.info("解决方案一:定时清理用户:{}的过期Session完成,执行耗时:{}",cleanSessionAccount,stopwatch);
}
return vueElementAdminResponse;
}
private VueElementAdminResponse cleanInvalidSessionSolutionTwo(VueElementAdminResponse vueElementAdminResponse,
Set<Object> allSessionIdSetList,
String principalKey,
String cleanSessionAccount){
Stopwatch stopwatch=Stopwatch.createStarted();
String traceId= vueElementAdminResponse.getSwiftNumber();
try {
for (Object currentSessionId:allSessionIdSetList){
String checkSessionId= (String)currentSessionId;
// 会话数据键 会自动过期
String sessionKey = DEFAULT_SPRING_SESSION_REDIS_PREFIX+REDIS_NAME_SPACE+":sessions:" + currentSessionId;
// 过期键 会自动过期
String expireKey = DEFAULT_SPRING_SESSION_REDIS_PREFIX+REDIS_NAME_SPACE+":sessions:expires:" + currentSessionId;
log.info("traceId:{},检测{}用户的session 过期Redis Key:{}",traceId,cleanSessionAccount,expireKey);
log.info("traceId:{},检测{}用户的session 数据Redis Key:{}",traceId,cleanSessionAccount,sessionKey);
// ttl 检测下是否过期
// 0:表示键已经过期。
//-1:表示键存在但没有设置过期时间。
//-2:表示键不存在。
Long expiration = this.sessionRepository.getSessionRedisOperations().getExpire(expireKey, TimeUnit.SECONDS);
// 检查会话是否已过期
if (expiration != null && expiration <= 0) {
// 删除会话数据键
this.sessionRepository.getSessionRedisOperations().delete(sessionKey);
log.info("traceId:{},删除{}用户过期的session数据redis key:{}",traceId,cleanSessionAccount,sessionKey);
// 删除过期键
this.sessionRepository.getSessionRedisOperations().delete(expireKey);
log.info("traceId:{},删除{}用户过期的session过期redis key:{}",traceId,cleanSessionAccount,expireKey);
// 删除索引键
this.sessionRepository.getSessionRedisOperations().boundSetOps(principalKey).remove(checkSessionId);
log.info("traceId:{},删除{}用户下redis Key:{}里面的sessionId:{}成功",traceId,cleanSessionAccount,principalKey,checkSessionId);
}
}
vueElementAdminResponse.setCode(ResponseCodeEnum.OK.getCode());
vueElementAdminResponse.setMessage("解决方案二:定时清理用户"+cleanSessionAccount+"的过期Session成功");
vueElementAdminResponse.setData(null);
} catch (Exception e) {
log.error("traceId:{},解决方案二:定时清理用户:{}的过期Session异常,异常详情:",traceId,cleanSessionAccount,e);
vueElementAdminResponse.setCode(50000);
vueElementAdminResponse.setMessage("解决方案二:定时清理用户"+cleanSessionAccount+"的过期Session异常");
vueElementAdminResponse.setData(null);
} finally {
stopwatch.stop();
log.info("traceId:{},解决方案二:定时清理用户:{}的过期Session完成,执行耗时:{}",traceId,cleanSessionAccount,stopwatch);
}
return vueElementAdminResponse;
}
}
0x02. 附录
2.1 用到的VO实体类
@Data
public class VueElementAdminResponse implements Serializable {
private static final long serialVersionUID = 3405402745489699953L;
private Integer code;
private String message;
private Object data;
private String swiftNumber=String.valueOf(SnowFlakeUtils.nextId());
}
2.2 雪花算法生成器工具类
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
/**
* id自增器(雪花算法)
* @author renjie
* @version 1.0.0
*/
public class SnowFlakeUtils {
private final static long twepoch = 12888349746579L;
/**
* 机器标识位数
*/
private final static long workerIdBits = 5L;
/**
* 数据中心标识位数
*/
private final static long datacenterIdBits = 5L;
/**
* 毫秒内自增位数
*/
private final static long sequenceBits = 12L;
/**
* 机器ID偏左移12位
*/
private final static long workerIdShift = sequenceBits;
/**
* 数据中心ID左移17位
*/
private final static long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 时间毫秒左移22位
*/
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* sequence掩码,确保sequnce不会超出上限
*/
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 上次时间戳
*/
private static long lastTimestamp = -1L;
private static long workerMask = -1L ^ (-1L << workerIdBits);
private static long processMask = -1L ^ (-1L << datacenterIdBits);
private static SnowFlakeUtils snowFlakeUtils = null;
static{
snowFlakeUtils = new SnowFlakeUtils();
}
/**
* 序列
*/
private long sequence = 0L;
/**
* 服务器ID
*/
private long workerId = 1L;
/**
* 进程编码
*/
private long processId = 1L;
private SnowFlakeUtils() {
//获取机器编码
this.workerId=this.getMachineNum();
//获取进程编码
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
this.processId=Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue();
//避免编码超出最大值
this.workerId=workerId & workerMask;
this.processId=processId & processMask;
}
public static synchronized long nextId(){
return snowFlakeUtils.getNextId();
}
public synchronized long getNextId() {
//获取时间戳
long timestamp = timeGen();
//如果时间戳小于上次时间戳则报错
if (timestamp < lastTimestamp) {
try {
throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
} catch (Exception e) {
e.printStackTrace();
}
}
//如果时间戳与上次时间戳相同
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1,与sequenceMask确保sequence不会超出上限
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
return nextId;
}
/**
* 再次获取时间戳直到获取的时间戳与现有的不同
* @param lastTimestamp
* @return 下一个时间戳
*/
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* 获取机器编码
* @return
*/
private long getMachineNum(){
long machinePiece;
StringBuilder sb = new StringBuilder();
Enumeration<NetworkInterface> e = null;
try {
e = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e1) {
e1.printStackTrace();
}
while (e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
sb.append(ni.toString());
}
machinePiece = sb.toString().hashCode();
return machinePiece;
}
}
更多推荐






所有评论(0)