Jason Lee Essay

Only dead fish go with the flow!

Jason Lee Essay

平时做mod运算时,一般习惯于以下的写法:

1
int a = b % c;

在很多的类库中都是类似这样的写法:

1
int a = b & 0xfffL;

第一种是正常的数学写法,第二种使用的是位运算。
位运算模式:
比如: 10 % 4 = 2

1
2
3
10 % 4 = 2;
// 转化为
10 & (4-1) = 2;

看到位运算为什么是4-1=3呢?

1
2
3
4
5
6
10 = 0b1010;
3 = 0b11;
// 计算过程:
1010
& 0011
= 0010 = 2;

使用&的是Java可表达的数字即可,多种进制均可使用:

  1. 0xfL =15 16进制
  2. 077 = 15 8进制
  3. 0b1111 = 15 2进制
  4. 15 10进制

性能

用JMH进行测试:
循环100次,mod32的结果

循环1000次,mod32的结果

循环10000次,mod32的结果

循环1000000次,mod32的结果

循环Intger.MAX_VALUE次,mod32的结果

结论

使用位运算,性能看起来没啥差别,按需选择就行了。

JVM是怎么实现%的呢?

今天遇到这样一个代码:

1
new DateTime("20231013").plusDays(1).toString("yyyy-MM-dd")

输出是什么?

预期输出:2023-10-14,实际输出:

不对,输出样式完全对不上。

换一种写法。

1
new DateTime("2023-10-13").plusDays(1).toString("yyyy-MM-dd")

这样就对了。

好像有点看出来了。第一个-前面的值当做年处理。

做个测试

1
new DateTime("20231013-10-13").plusDays(1).toString("yyyy-MM-dd")

可以确定,new DateTime 的入参是有个格式要求的。

全景:

1
2
3
<em>log</em>.info( new DateTime("20231013").plusDays(1).toString("yyyy-MM-dd"));
<em>log</em>.info( new DateTime("2023-10-13").plusDays(1).toString("yyyy-MM-dd"));
<em>log</em>.info( new DateTime("20231013-10-13").plusDays(1).toString("yyyy-MM-dd"));

后记

new DateTime 没有格式化入参的构造方法,使用时要注意格式化失败的问题。

select for update 是如何锁定表的?

for update 定义

for update 是一种行级锁,又叫排它锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则也必须对该表施加行级锁.即使多个用户对一个表均使用了共享更新,但也不允许两个事务同时对一个表进行更新,真正对表进行更新时,是以独占方式锁表,一直到提交或复原该事务为止。行锁永远是独占方式锁。

只有当出现如下之一的条件,才会释放共享更新锁:

1、执行提交(COMMIT)语句

2、退出数据库(LOG OFF)

3、程序停止运行

概念和用法

通常情况下,select 语句是不会对数据加锁,妨碍影响其他的 DML 和 DDL 操作。同时,在多版本一致读机制的支持下,select 语句也不会被其他类型语句所阻碍。

而 select … for update 语句是我们经常使用手工加锁语句。在数据库中执行 select … for update ,大家会发现会对数据库中的表或某些行数据进行锁表,在 mysql 中,如果查询条件带有主键,会锁行数据,如果没有,会锁表。

由于 InnoDB 预设是 Row-Level Lock,所以只有「明确」的指定主键,MySQL 才会执行 Row lock (只锁住被选取的资料例) ,否则 MySQL 将会执行 Table Lock (将整个资料表单给锁住)。

注意:
1、FOR UPDATE 仅适用于 InnoDB,且必须在事务处理模块(BEGIN/COMMIT)中才能生效。

2、要测试锁定的状况,可以利用 MySQL 的 Command Mode(命令模式) ,开两个视窗来做测试。

验证:

开启事务

查看锁:

1
select *from information_schema.INNODB_TRX;

开启新窗口执行加锁或更新操作:

1
2
3
4
5
6
set session transaction isolation level repeatable read;
set autocommit=0;
begin ;
select *from xxl_job_lock t ; // 可执行
select *from xxl_job_lock t where lock_name='schedule_lock' for update; // 等待锁
update xxl_job_lock set lock_name='schedule_lock3' where lock_name='schedule_lock'; // 等待锁

MySQL 的事务隔离级别?

如何查看 mysql 中的事务隔离级别:

1
2
3
查看系统隔离级别:select @@global.tx_isolation;
查看会话隔离级别(5.0以上版本):select @@tx_isolation;
查看会话隔离级别(8.0以上版本):select @@transaction_isolation;

Jetbrains Idea 是常用的开发工具,在创建类时,很多时候需要添加一些通用类注释,比如作者、时间等,可以通过设置类模板来实现。
打开Settings -> Editor -> File and Code Templates
找到 Class 和 Interfaces 对应java的类和接口文件。

支持的变量列表:

  • ${PACKAGE_NAME} Name of the package in which a new class is created 包名称
  • ${NAME} Name of the new class specified by you in the Create New Class dialog 类名称
  • ${USER} System login name of the current user 当期登录名
  • ${DATE} Current system date 日期 2024/1/10
  • ${TIME} Current system time 时间 14:53
  • ${YEAR} Current year 年
  • ${MONTH} Current month 月 数字
  • ${MONTH_NAME_SHORT} First 3 letters of the current month name (Jan, Feb, and so on) 简写月 英文
  • ${MONTH_NAME_FULL} Full name of the current month (January, February, and so on) 全写月 英文
  • ${DAY} Current day of the month 日
  • ${HOUR} Current hour 小时
  • ${MINUTE} Current minute 分
  • ${PROJECT_NAME} Name of the current project 当期项目名称

一个配置样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")

import lombok.extern.slf4j.Slf4j;
/**
* @Title:
* @Description:
* @author: libo
* @date: ${DATE} ${TIME}
* @Version:1.0
*/
@Slf4j
public class ${NAME} {
}

新建类测试下

Statemachine即状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。

四大概念

下面来给出状态机的四大概念。

  1. State ,状态。一个状态机至少要包含两个状态。例如上面自动门的例子,有 open 和 closed 两个状态。
  2. Event ,事件。事件就是执行某个操作的触发条件或者口令。对于自动门,“按下开门按钮”就是一个事件。
  3. Action ,动作。事件发生以后要执行动作。例如事件是“按开门按钮”,动作是“开门”。编程的时候,一个 Action 一般就对应一个函数。
  4. Transition ,变换。也就是从一个状态变化为另一个状态。例如“开门过程”就是一个变换。

从另一个角度来说,

flowchart LR
   现态\nState-1 == 变换\nTransition ==> 次态\nState-2

Event则是启动变换的因,action是变换的具体实现,次态是变换的结果。

Spring Statemachine is a framework for application developers to use state machine concepts with Spring applications. Spring Statemachine 是应用程序开发人员在Spring应用程序中使用状态机概念的框架。

Spring Statemachine 提供如下特色:

  • Easy to use flat one level state machine for simple use cases.(易于使用的扁平单级状态机,用于简单的使用案例。)
  • Hierarchical state machine structure to ease complex state configuration.(分层状态机结构,以简化复杂的状态配置。)
  • State machine regions to provide even more complex state configurations.(状态机区域提供更复杂的状态配置。)
  • Usage of triggers, transitions, guards and actions.(使用触发器、transitions、guards和actions。)
  • Type safe configuration adapter.(应用安全的配置适配器。)
  • State machine event listeners.(状态机事件监听)
  • Spring IOC integration to associate beans with a state machine.(Spring IOC集成将bean与状态机关联起来)

现象

代码定义个 Listner 为了,为了预加载一些外部配置。

明显可以看到在 27 分和 30 分分别执行了 1 次。

代码分析

第 1 次由 spring-boot 加载,prepareContext 时调用。

org/springframework/boot/spring-boot/2.3.3.RELEASE/spring-boot-2.3.3.RELEASE-sources.jar!/org/springframework/boot/SpringApplication.java:393

第 2 次调用,依然有 spring-boot 发起,在 refreshContext 阶段发起

调用到了 RestartListener,RestartListener 会再次发布 PrepareEvent

而 RestartListener 是 spring-cloud 包自动通过 spring.factories 文件引入的

解决这个问题

这个应该属于 bug 类问题,当我用到了 ApplicationPrepareEvent 时,意味着我必须前置执行。为啥要做 2 次执行,暂时处理逻辑时内部做幂等判断。

众所周知gitlab对中国区不友好,无法直接注册,页面无法选择+86的手机号进行验证码发送。
Google上众多的方案是修改dom,而且时间大约是21年以前。
修改dom,对于现在的VUE、React框架来说是没有用的,所以不用尝试。
直接看请求,用魔法打败魔法。
首先看一下请求,有个叫做countries的请求,看起来像,看下内容:

看内容就是它了,再看看他是怎么加载的。

进入代码:
随便挑一条,改一下为86,

已经可以选了,后续操作正常进行。

win11 开发环境搭建

环境准备

  • python sdk

  • Pycharm - 开发IDE

下载 python sdk :

https://www.python.org/downloads/windows/

要下载 Windows installer , 不要 embeddable package 。

embeddable package 的 pip 安装有问题,找不到解决办法。

pycharm 自行下载安装即可。

项目示例:

Pycharm 新建项目:

首先要配置一个 interpreter:

interpreter 直译为解释器,即在当期环境下的解释器及依赖包管理。类比与 java 中的 maven \gradle ,nodejs 中的 npm + package.json。

一般选择 virtualenv 即可。

运行下 py 文件,看下结果:

1
Hi, PyCharm lll

至此基本环境搭建完成。

本软件尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更有个性化的服务,本软件会按照本隐私权政策的规定使用和披露您的个人信息。但本软件将以高度的勤勉、审慎义务对待这些信息。除本隐私权政策另有规定外,在未征得您事先许可的情况下,本软件不会将这些信息对外披露或向第三方提供。本软件会不时更新本隐私权政策。您在同意本软件服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于本软件服务使用协议不可分割的一部分。

1.适用范围

a)在您使用本软件网络服务,本软件自动接收并记录的您的手机上的信息,包括但不限于您的健康数据、使用的语言、访问日期和时间、软硬件特征信息及您需求的网页记录等数据;

2.信息的使用

a)在获得您的数据之后,本软件会将其上传至服务器,以生成您的排行榜数据,以便您能够更好地使用服务。

3.信息披露

a)本软件不会将您的信息披露给不受信任的第三方。

b)根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;

c)如您出现违反中国有关法律、法规或者相关规则的情况,需要向第三方披露;

4.信息存储和交换

本软件收集的有关您的信息和资料将保存在本软件及(或)其关联公司的服务器上,这些信息和资料可能传送至您所在国家、地区或本软件收集信息和资料所在地的境外并在境外被访问、存储和展示。

5.信息安全

a)在使用本软件网络服务进行网上交易时,您不可避免的要向交易对方或潜在的交易对方披露自己的个人信息,如联络方式或者邮政地址。请您妥善保护自己的个人信息,仅在必要的情形下向他人提供。如您发现自己的个人信息泄密,请您立即联络本软件客服,以便本软件采取相应措施。

雪花 id 是一个 64bit 的 long 型 id。

snowflake 是 Twitter 开源的分布式 ID 生成算法,结果是 64bit 的 Long 类型的 ID,有着全局唯一和有序递增的特点。

  • 最高位是符号位,因为生成的 ID 总是正数,始终为 0,不可用。
  • 41 位的时间序列,精确到毫秒级,41 位的长度可以使用 69 年。时间位还有一个很重要的作用是可以根据时间进行排序。
  • 10 位的机器标识,10 位的长度最多支持部署 1024 个节点。
  • 12 位的计数序列号,序列号即一系列的自增 ID,可以支持同一节点同一毫秒生成多个 ID 序号,12 位的计数序列号支持每个节点每毫秒产生 4096 个 ID 序号。
    今天我们来探究下 2 种常用 jar 包里面的 snowflake 的实现差异。
    除了标准实现有很多种实现方式,不同方式的实现有不同的差异,主要体现在时间、数据中心、workId、sequence 的 4 段长度差异上。

生成雪花 id

号段分配

Mybatis-plus

mybatis-plus 自带了 IdentifierGenerator 的接口,提供了 ImadcnIdentifierGenerator 和 DefaultIdentifierGenerator 两种实现。

其中 DefaultIdentifierGenerator 是大多数情况的默认实现。

mybatis-plus 的实现与原版实现相比加入了 datacenterId 的概念,而且也引入了基准时间 1288834974657L;

标志位 时间 datacenterId workId sequence
1 41 5 5 12

Sharding-jdbc

sharding 的实现类叫做:SnowflakeShardingKeyGenerator

sharding 的实现与原版实现相比没有特别差异,而且基准时间也有差异

标志位 时间 workId sequence
1 41 10 12

生成逻辑差异

Mybatis-plus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public synchronized long nextId() {
long timestamp = timeGen();
//闰秒
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
wait(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
}

if (lastTimestamp == timestamp) {
// 相同毫秒内,序列号自增
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 同一毫秒的序列数已经达到最大
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒内,序列号置为 1 - 3 随机数
sequence = ThreadLocalRandom.current().nextLong(1, 3);
}

lastTimestamp = timestamp;

// 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}

最有一部分要记住,反解析时需使用。

生成几个 id 看看:参数 = datacenterId=22&workId=11&count=10

1
2
3
4
5
6
7
8
9
10
1722086321019465730,
1722086321019465731,
1722086321019465732,
1722086321019465733,
1722086321019465734,
1722086321019465735,
1722086321019465736,
1722086321019465737,
1722086321019465738,
1722086321019465739

Sharding-jdbc

直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public synchronized Comparable<?> generateKey() {
long currentMilliseconds = timeService.getCurrentMillis();
if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
currentMilliseconds = timeService.getCurrentMillis();
}
if (lastMilliseconds == currentMilliseconds) {
if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
currentMilliseconds = waitUntilNextTime(currentMilliseconds);
}
} else {
vibrateSequenceOffset();
sequence = sequenceOffset;
}
lastMilliseconds = currentMilliseconds;
return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}

核心在于最后一行,((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence; 这个要记住,反解析时要用到。

生成几个 id 看看:workId=555&type=sharding&count=10

1
2
3
4
5
6
7
8
9
10
928955622318321665,
928955622318321666,
928955622318321667,
928955622318321668,
928955622318321669,
928955622318321670,
928955622318321671,
928955622318321672,
928955622318321673,
928955622318321674

反解析

Mybatis-plus

反解析就是把生成的逻辑反过来,按照号段分配逻辑,按位解析,注意:时间因为有基准时间,所以必须反向加减。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void buildMybatis(Map<String, Object> res, String sonwFlakeId) {
/**
* 机器标识位数
*/
final long twepoch = 1288834974657L;
/**
* 机器标识位数
*/
final long workerIdBits = 5L;
final long datacenterIdBits = 5L;
/**
* 毫秒内自增位
*/
final long sequenceBits = 12L;
final long workerIdShift = sequenceBits;
final long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 时间戳左移动位
*/
final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
Map<String, Object> mybatisMap = new HashMap<>();
res.put("mybatis-plus", mybatisMap);
int len = sonwFlakeId.length();
int sequenceStart = (int) (len < workerIdShift ? 0 : len - workerIdShift);
int workerStart = (int) (len < datacenterIdShift ? 0 : len - datacenterIdShift);
int timeStart = (int) (len < timestampLeftShift ? 0 : len - timestampLeftShift);
String sequence = sonwFlakeId.substring(sequenceStart, len);
String workerId =
sequenceStart == 0 ? "0" : sonwFlakeId.substring(workerStart, sequenceStart);
String dataCenterId =
workerStart == 0 ? "0" : sonwFlakeId.substring(timeStart, workerStart);
String time = timeStart == 0 ? "0" : sonwFlakeId.substring(0, timeStart);
int sequenceInt = Integer.valueOf(sequence, 2);
mybatisMap.put("sequence", sequenceInt);
int workerIdInt = Integer.valueOf(workerId, 2);
mybatisMap.put("workerId", workerIdInt);
int dataCenterIdInt = Integer.valueOf(dataCenterId, 2);
mybatisMap.put("dataCenter", dataCenterIdInt);
long diffTime = Long.parseLong(time, 2);
long timeLong = diffTime + twepoch;
String date = DateFormatUtils.format(timeLong, "yyyy-MM-dd HH:mm:ss");
mybatisMap.put("date", date);
}

运行结果:1722086321019465730 的解析结果:

1
2
3
4
"date": "2023-11-08 10:59:08",
"sequence": 2,
"workerId": 11,
"dataCenter": 22

1722086321019465731 的解析结果:

1
2
3
4
"date": "2023-11-08 10:59:08",
"sequence": 3,
"workerId": 11,
"dataCenter": 22

Sharding-jdbc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void buildShardingJdbc(Map<String, Object> res, String sonwFlakeId) {
final long SEQUENCE_BITS = 12L;
final long WORKER_ID_BITS = 10L;
Calendar calendar = Calendar.getInstance();
calendar.set(2016, Calendar.NOVEMBER, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Long EPOCH = calendar.getTimeInMillis();
final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
Map<String, Object> dataMap = new HashMap<>();
res.put("sharding-jdbc", dataMap);
int len = sonwFlakeId.length();
int sequenceStart = (int) (len < SEQUENCE_BITS ? 0 : len - SEQUENCE_BITS);
int workerStart = (int) (len < TIMESTAMP_LEFT_SHIFT_BITS ? 0
: len - TIMESTAMP_LEFT_SHIFT_BITS);
String sequence = sonwFlakeId.substring(sequenceStart, len);
String workerId =
sequenceStart == 0 ? "0" : sonwFlakeId.substring(workerStart, sequenceStart);
String time = workerStart == 0 ? "0" : sonwFlakeId.substring(0, workerStart);
int sequenceInt = Integer.valueOf(sequence, 2);
dataMap.put("sequence", sequenceInt);
int workerIdInt = Integer.valueOf(workerId, 2);
dataMap.put("workerId", workerIdInt);
long diffTime = Long.parseLong(time, 2);
long timeLong = diffTime + EPOCH;
String date = DateFormatUtils.format(timeLong, "yyyy-MM-dd HH:mm:ss");
dataMap.put("date", date);
}

解析结果:928955622318321665

1
2
3
"date": "2023-11-08 10:17:59",
"sequence": 1,
"workerId": 555

928955622318321666

1
2
3
"date": "2023-11-08 10:17:59",
"sequence": 2,
"workerId": 555

总结

经过上面的反解析可以发现 1 个隐藏规律:

mybatis-plus 生成的 id 是以 1 开头的,而 sharding-jdbc 生成的是以 9 开头的,原因在于 2 个的时间基础不同。

0%