一、数据库自增id
1.自增id含义
数据库中的自增ID是指在插入新记录时,数据库系统会自动为该记录生成一个唯一的标识符,并且这个标识符会按照一定规则(通常是递增)进行自动生成。这个自动生成的唯一标识符通常用作表的主键,以确保每条记录都有一个唯一的标识
2.不同数据库实现自增ID的方式
1.MySQL / MariaDB:
在 MySQL 和 MariaDB 中,可以使用 AUTO_INCREMENT 关键字来指定某个字段为自增ID。例如
CREATE TABLE example (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100)
);
2.PostgreSQL:
在 PostgreSQL 中,可以使用 SERIAL 类型来实现自增ID,也可以使用序列(Sequence)。例如:
CREATE TABLE example (
id SERIAL PRIMARY KEY,
name VARCHAR(100)
);
3.SQL Server:
在 SQL Server 中,可以使用 IDENTITY 关键字来指定自增ID。例如:
CREATE TABLE example (
id INT IDENTITY(1,1) PRIMARY KEY,
name VARCHAR(100)
);
4.Oracle:
在 Oracle 数据库中,可以使用序列(Sequence)和触发器(Trigger)来实现类似于自增ID的功能。例如:
CREATE SEQUENCE example_seq START WITH 1 INCREMENT BY 1;
CREATE TABLE example (
id NUMBER DEFAULT example_seq.NEXTVAL PRIMARY KEY,
name VARCHAR2(100)
);
二、雪花ID
1.雪花ID生成工具类
public class SnowUtils {
/** 开始时间截 (2021-10-01) */
private final long twepoch = 1633017600000L;
/** 机器id所占的位数 */
private final long machineIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxMachineId = -1L ^ (-1L << machineIdBits);
/** 数据标识id所占的位数 */
private final long machineRoomIdBits = 5L;
/** 支持的最大数据标识id,结果是31 */
private final long maxMachineRoomId = -1L ^ (-1L << machineRoomIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long machineIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long machineRoomIdShift = sequenceBits + machineIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + machineIdBits + machineRoomIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
/**
* 构造函数
* machineId 工作ID (0~31)
* machineRoomId 数据中心ID (0~31)
*/
public SnowUtils(@Autowired Params params) {
if (params.getMachineId() > maxMachineId || params.getMachineId() < 0) {
throw new IllegalArgumentException("机器ID错误");
}
if (params.getMachineRoomId() > maxMachineRoomId || params.getMachineRoomId() < 0) {
throw new IllegalArgumentException("机房ID错误");
}
}
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (机房ID << machineRoomIdShift) //
| (机器ID << machineIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
/**
* id转16进制
* @param id
* @return 返回16进制字符串
*/
public static String idToHex(Long id){
return Long.toHexString(id);
}
}
2.基础使用
1.针对对象设置ID
User user = new User();
user.setId(snowUtils.nextId());
2.写入数据库
<insert id="insert" keyProperty="id" useGeneratedKeys="true">
insert into sys_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="userName !=null and '' != userName">
user_name,
</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id},
</if>
<if test="userName !=null and '' != userName">
#{userName},
</if>
</trim>
</insert>
注意: mapper层一定要有ID的设置值,不然ID无法写入数据库,如果数据库没有设置自增ID将会出现异常。
3.分布式统一设置ID
1.了解拦截器
MyBatis 中的 @Intercepts 和 @Signature 注解,用于实现对 MyBatis 方法的拦截和处理。@Intercepts 注解用于指定一个拦截器,@Signature 注解用于指定要拦截的方法签名。
通常情况下,@Intercepts 注解需要和 @Signature 注解一起使用,@Signature 注解用于指定要拦截的方法的详细信息,包括方法名、方法参数列表等。通过这两个注解的结合使用,可以实现对 MyBatis 方法的拦截功能,比如添加日志、权限控制等操作。
2.编写拦截器
@Intercepts({
@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class,method = "update",args = {MappedStatement.class, Object.class}),
})
public class ExecutorInterceptor implements Interceptor {
private SnowUtils snowUtils;
public ExecutorInterceptor(SnowUtils snowUtils){
this.snowUtils = snowUtils;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement)args[0];
Object param = args[1];
Map<String,Object> map;
if(param == null){
map = new HashMap<>();
}else if(param instanceof Map){
map = (Map)param;
}else if(param.getClass().isPrimitive()
|| param instanceof String
|| param instanceof Integer
|| param instanceof Double
|| param instanceof Float
|| param instanceof Long
|| param instanceof Boolean
|| param instanceof Byte
|| param instanceof Short){
map = new HashMap<>();
String id = mappedStatement.getId();
String clazzName = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".")+1);
Class<?> clazz = Class.forName(clazzName);
for(Method method: clazz.getMethods()){
if(method.getName().equals(methodName)){
Parameter parameter = method.getParameters()[0];
Param annotation = parameter.getAnnotation(Param.class);
if(annotation != null){
map.put(annotation.value(),param);
}else {
map.put(parameter.getName(), param);
}
}
}
}else {
map = new HashMap<>();
Class<?> clazz = param.getClass();
while (clazz != null) {
Field[] declaredFields = clazz.getDeclaredFields();
for(Field field: declaredFields){
field.setAccessible(true);
if(!map.containsKey(field.getName())){
map.put(field.getName(),field.get(param));
}
}
clazz = clazz.getSuperclass();
}
}
if(mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT) && map.containsKey("id") && (map.get("id") == null || String.valueOf(map.get("id")).trim().equals("0")) ){
map.put("id",Long.valueOf(snowUtils.nextId()));
}
args[1] = map;
MybatisHolder.set(param);
return invocation.proceed();
}
}
3.解决问题
上述拦截之后参数会变成一个map,返回之后无法从里面获取内应内容,也就是无法获取返回的主键,对应mapper层的返回参数为null;可以添加一个拦截器来把map转为params。
@Intercepts({
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})
})
public class StatementInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object param = MybatisHolder.get();
Object target = invocation.getTarget();
Object result = invocation.proceed();
if(target instanceof RoutingStatementHandler){
Field field = RoutingStatementHandler.class.getDeclaredField("delegate");
field.setAccessible(true);
PreparedStatementHandler statementHandler = (PreparedStatementHandler) field.get(target);
Field declaredField = BaseStatementHandler.class.getDeclaredField("mappedStatement");
declaredField.setAccessible(true);
MappedStatement mappedStatement = (MappedStatement) declaredField.get(statementHandler);
String[] keyProperties = mappedStatement.getKeyProperties();
if(keyProperties != null){
ParameterHandler parameterHandler = statementHandler.getParameterHandler();
Object parameter = parameterHandler.getParameterObject();
if(parameter instanceof Map){
Map map = (Map)parameter;
for(String key: keyProperties){
Field paramField = param.getClass().getDeclaredField(key);
paramField.setAccessible(true);
Object value = map.get(key);
if(value instanceof BigInteger){
BigInteger bigInteger = (BigInteger) value;
long longValue = bigInteger.longValue();
paramField.set(param,longValue);
}else {
paramField.set(param,value);
}
}
}
}
}
MybatisHolder.remove();
return result;
}
}
4.添加配置
@Configuration
@Order(1)
public class CommandLineRunnerConfig implements CommandLineRunner {
@Resource
private SqlSessionFactory sqlSessionFactory;
@Resource
private SnowUtils snowUtils;
@Override
public void run(String... args){
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.addInterceptor(new ExecutorInterceptor(snowUtils));
configuration.addInterceptor(new StatementInterceptor());
}
}
以上就是我所知道的JAVA设置ID,不足之处望海涵。