结论:
WebConfiguration活干的太多,注入了很多业务类(用户类),而业务类里面也通过@Autowired注入了 private SnowFlake snowFlake;导致在WebConfiguration加载中,需要先执行
@Bean
public SnowFlake snowFlake() {
return new SnowFlake(workerId, datacenterId);
}
而此时 @Value("${snow.flake.worker.id}") private long workerId; @Value("${snow.flake.datacenter.id}") private long datacenterId; 在业务类下面,还未注入成功,所以默认为0。
前言:
在接手维护一个别人的项目后,在启动日志里面,发现雪花算法的workerId和datacenterId一直都是0,与配置文件中的数字不一致。
Worker starting. timestamp left shift 11, datacenter id bits 3,
worker id bits 3, sequence bits 5, workerid 0 ,datacenterId 0
这个问题如果是多台服务器在业务繁忙时应该会出现id碰撞,导致部分数据插入失败。
过程:
先开始排查雪花算法问题。
public class SnowFlake {
//下面两个每个5位,加起来就是10位的工作机器id
private long workerId; //工作id
private long datacenterId; //数据id
//初始时间戳
private long twepoch = 1577808000000L;
//长度为5位
private long datacenterIdBits = 3L;
private long workerIdBits = 3L;
//最大值
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
//序列号id长度
private long sequenceBits = 5L;
//序列号最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//初始化的序列号
private long sequence = 1;
//工作id需要左移的位数,12位
private long workerIdShift = sequenceBits;
//数据id需要左移位数 12+5=17位
private long datacenterIdShift = sequenceBits + workerIdBits;
//时间戳需要左移位数 12+5+5=22位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//上次时间戳,初始值为负数
private long lastTimestamp = -1L;
public SnowFlake(long workerId, long datacenterId) {
this.workerId = workerId;
this.datacenterId = datacenterId;
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d ,datacenterId %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId, datacenterId);
System.out.println("");
}
//下一个ID生成算法
public synchronized long nextId() {
long timestamp = timeGen();
//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
//获取时间戳,并与上次时间戳比较
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//单次获取雪花id
public static long getSnowflakeId(){
SnowFlake worker = new SnowFlake(1, 1);
return worker.nextId();
}
// 获取系统时间戳
private long timeGen() {
return System.currentTimeMillis();
}
}
通过排查雪花算法的id发现没有什么问题,workerId和datacenterId值正常——通过下面回推结果正确。
// 从雪花ID中提取时间戳
public long extractTimestamp(long id) {
return (id >> timestampLeftShift) + twepoch;
}
// 从雪花ID中提取工作机器ID
public long extractWorkerId(long id) {
return (id >> workerIdShift) & ((1L << workerIdBits) - 1);
}
// 从雪花ID中提取数据中心ID
public long extractDatacenterId(long id) {
return (id >> datacenterIdShift) & ((1L << datacenterIdBits) - 1);
}
public static void main(String[] args) {
SnowFlake worker = new SnowFlake(1, 3);
long id = worker.nextId();
long timestamp = decoder.extractTimestamp(id);
long workerId = decoder.extractWorkerId(id);
long datacenterId = decoder.extractDatacenterId(id);
System.out.println("");
System.out.println("Timestamp: " + timestamp);
System.out.println("Worker ID: " + workerId);
System.out.println("Datacenter ID: " + datacenterId);
}
继续排查,项目中SnowFlake的初始化是在WebConfiguration中通过@Bean注入的。如下代码所示:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired
private EmployeeService employeeService;
@Autowired
private CustomerService customerService;
@Autowired
private ***Service ***Service;
//...
@Value("${snow.flake.worker.id}")
private long workerId;
@Value("${snow.flake.datacenter.id}")
private long datacenterId;
@Bean
public CommonExceptionHandler centerExceptionHandler() {
return new CommonExceptionHandler();
}
@Bean
public SnowFlake snowFlake() {
return new SnowFlake(workerId, datacenterId);
}
@Bean
public JwtComponent jwtComponent() {
return new JwtComponent(appName, 7 * 24 * 60 * 60 * 1000L, null);
}
@Bean
public IdentityFilter identityFilter(JwtComponent jwtComponent) {
return new IdentityFilter(workUnitAssembler(), jwtComponent
, null
);
}
@Bean
public WorkUnitAssembler workUnitAssembler(){
employeeService...
customerService...
}
}
通过调试和打印参数,得到每次 snowFlake() 是最先执行的,改变代码顺序也不影响执行顺序(PS:此时以为是调度问题,未能想到是注入顺序问题,导致后面浪费很多时间)。每次workerId, datacenterId都没有注解成功,默认值为0,在其他@Bean执行时workerId, datacenterId又都获取到值。此事this对象只有employeeService被注入成功,后面都为null。(PS:其实这时候也能判断出一些问题了)
当时猜测可能是SnowFlake执行顺序高于@Vlaue注解顺序。为了验证又重新编写了一个测试的类,如下
@Configuration
public class TestConfiguration {
@Autowired
private EmployeeService employeeService;
@Value("${snow.flake.worker.id}")
private long workerId; //工作id
@Value("${snow.flake.datacenter.id}")
private long datacenterId; //数据id
private static final Integer test = 3;
static {
System.out.println("1 >> " + test);
}
public TestConfiguration() {
System.out.println("2 >> " + workerId + "test:" + test );
}
@Bean
public void test(){
System.out.println("5 >>");
employeeService.queryByUserId("1");
}
@Bean
public JwtComponent jwtComponent() {
System.out.println("4 >> " + workerId);
return new JwtComponent("test", 7 * 24 * 60 * 60 * 1000L, null);
}
@Bean
public SnowFlake snowFlake() {
System.out.println("3 >> " + workerId);
return new SnowFlake(workerId, datacenterId);
}
public TestConfiguration(long workerId) {
this.workerId = workerId;
System.out.println("6 >> " + workerId + "test:" + test);
}
public static void uTest() {
System.out.println("7 >> ");
}
}
得到的打印顺序是:
1 >> 3
2 >> 0 test:3
3 >> 2
4 >> 2
5 >>
通过打印顺序发现@Value注入并没有问题,那么推测可能是与WebConfiguration的一些问题相关。(PS:其实这里如果稍微转换思路就知道问题了,但是由于基础薄弱,还是没有发现根本问题,以为是别的底层原因。)
重新回到WebConfiguration中,继续排查。
如果将@Value放置在代码的执行上方,SnowFlake注入时又可以获取到值。
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Value("${snow.flake.worker.id}")
private long workerId;
@Value("${snow.flake.datacenter.id}")
private long datacenterId;
@Autowired
private EmployeeService employeeService;
...
}
到这里,当时感觉问题解决了。。。
当时表象是将@Value不能放置在多个(大于等于2)@Autowired后面
写到这里感觉还是代码逻辑问题,再编写TestConfiguration验证。
发现如果后面的Bean不实现@Autowired,那么不限定于1个或者多个,还是可以注入。如果在后面的@Bean都是后相互依赖的,可能是Spring以为暂时不需要注入影响了后面的@Value的注入。
@Configuration
public class TestConfiguration {
@Autowired
private EmployeeService employeeService;
@Autowired
private CustomerService customerService;
@Value("${snow.flake.worker.id}")
private long workerId; //工作id
@Value("${snow.flake.datacenter.id}")
private long datacenterId; //数据id
@Bean
public SnowFlake snowFlake() {
return new SnowFlake(workerId, datacenterId);
}
@Bean
public JwtComponent jwtComponent() {
return new JwtComponent("appName", 7 * 24 * 60 * 60 * 1000L, null);
}
@Bean
public IdentityFilter identityFilter(JwtComponent jwtComponent) {
return new IdentityFilter(workUnitAssembler(), jwtComponent
, null // 日志收集
);
}
@Bean
public WorkUnitAssembler workUnitAssembler(){
return identityWhole -> {
if (identityWhole.getUserType() == UserTypeEnum.Employee.getCode()) {
EmployeePO employeePo = employeeService.query(identityWhole.getUserId());
} else if (identityWhole.getUserType() == UserTypeEnum.Customer.getCode()) {
CustomerPO customerPo = customerService.query(identityWhole.getUserId());
}
};
}
}
以为是
@Bean
public IdentityFilter identityFilter(JwtComponent jwtComponent) {
return new IdentityFilter(workUnitAssembler(), jwtComponent
, null // 日志收集
);
}
影响了spring对@Value的注入。如果注释掉此处代码,spring会将所有@Autowired和@Value进行注入。应该是spring的管理策略导致的问题。
public class IdentityFilter extends OncePerRequestFilter {
...
}
到这里终于才要接近真相了。
----------------------------------------------------------分割线-----------------------------------------------------------
最后的原因是其他用户类中使用了,而员工类没有使用。(PS:潜意识一直以为都是一样的)
@Autowired
private SnowFlake snowFlake;
所以需要先注入
@Bean
public SnowFlake snowFlake() {
return new SnowFlake(workerId, datacenterId);
}
同时workerId, datacenterId在这些用户类注入的下面,所以代码没有执行到赋值这步,所以默认为零。
参考文章: