在这里插入图片描述

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 键来存储每个会话的数据
    1. 这个键存储会话的实际数据,包括所有会话属性。键名格式如下:
spring:session:${redisNamespace}:sessions:<sessionId>
    1. 这个键用于存储会话的过期时间,便于 Redis 定期清理过期会话
spring:session:${redisNamespace}:sessions:expires:<expireTimestamp>:<sessionId>
    1. 这个键用于存储会话的索引,例如,按用户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)已经过期,系统仍然会给这些会话一个额外的五分钟,
以处理延迟或确保系统有时间清理和处理这些过期会话。
这种策略可以在保证会话及时过期的同时,为系统管理和清理会话提供一定的缓冲时间。

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
    是固定的框架定义的key
  • testAccount: 绑定的系统账号

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;
    }
}

更多推荐