1.使用自增主键的弊端
首先在实际工程中我们很少用1,2,3......这样的自增主键,原因如下:
- 主键冲突
- 性能问题
- 安全问题
主键冲突:
比如我要跨数据库进行数据同步、或者在分布式系统中跨“分区”进行数据同步,不难想象,1,2,3......这种递增的单数字是极容易产生冲突的。
性能问题:
自增主键在数据库中使用的是自增序列来生成主键值,而在高并发情况下,多个线程可能同时请求获取下一个自增值,此时会发生竞争,因为数据库需要保证每个自增值只会被分配一次。为了保证自增值的唯一性,数据库使用锁机制来避免多个线程同时获取同一个自增值。
安全问题:
使用自增主键可以很容易地猜测出下一条记录的主键值,这可能会导致一些安全问题。
综上所述,我们可以发现使用复杂主键是很有必要的,要生成复杂且具有唯一性的主键就需要依赖主键生成算法了。
2.主键生成算法
常见的主键生成算法:
- UUID
- 雪花算法
2.1.UUID
2.1.1.概述
UUID,通用唯一标识符,UUID算法的核心思想是生成一个128位的唯一标识符,通常表示为32个十六进制数字的字符串。每个UUID会尽量保证其唯一性。
UUID算法目前有几个官方推出的版本,由Internet工程任务组(IETF)、国际标准化组织(ISO)和国际电信联盟(ITU)共同制定:
UUIDv1:基于时间戳和MAC地址生成UUID,保证同一台计算机上生成的UUID是唯一的,但不适合在分布式系统中使用。
UUIDv2:基于DCE安全机制的UUID,使用POSIX的UID/GID和当前时间生成UUID,不常用。
UUIDv3:基于命名空间和字符串生成UUID,使用MD5散列算法生成UUID。
UUIDv4:使用随机数生成UUID,保证在所有计算机上都是唯一的。
UUIDv5:与UUIDv3类似,但使用SHA-1散列算法生成UUID。
其实本质上来说UUID只是个概念,UUID的算法我们甚至可以自己去写一个,只是我们自己写的肯定会没那么优质,保证唯一性的效果不会好。
2.1.2.JAVA中的UUID
工程上我们一般都是在JAVA后端生成UUID作为每条要插入数据库的数据的主键,JAVA中生成UUID很简单:
很简单:
import java.util.UUID; public class UUIDExample { public static void main(String[] args) { // 生成一个随机的UUID UUID uuid = UUID.randomUUID(); System.out.println("Random UUID: " + uuid.toString()); } }
生成过程其实也很简单:
public static UUID randomUUID() { SecureRandom ng = Holder.numberGenerator; byte[] randomBytes = new byte[16]; ng.nextBytes(randomBytes); randomBytes[6] &= 0x0f; /* clear version */ randomBytes[6] |= 0x40; /* set to version 4 */ randomBytes[8] &= 0x3f; /* clear variant */ randomBytes[8] |= 0x80; /* set to IETF variant */ return new UUID(randomBytes); }
首先从Holder静态内部类中获取一个SecureRandom对象。Holder类是一个懒加载的单例类,用于确保SecureRandom对象只被创建一次,并且线程安全。
然后生成一个长度为16的字节数组randomBytes,使用ng.nextBytes(randomBytes)方法填充随机数。
接下来将字节数组randomBytes中的一些特定位进行修改,以符合UUID版本4的格式要求:
将第7个字节的高4位清零,再将其低4位设置为4,表示这是UUID版本4。
将第9个字节的高2位清零,再将其第7位设置为1,表示这是符合IETF标准的UUID。
最后,将修改后的字节数组作为参数,调用UUID(byte[] data)构造方法生成一个UUID对象。
2.2.雪花算法
2.2.1.概述
雪花算法(Snowflake Algorithm)是Twitter公司开发的一种分布式ID生成算法,用来生成64位的唯一ID。它的核心思想是:利用一个64位的long型数字作为全局唯一ID,高位部分表示时间戳,中间部分表示机器ID,低位部分表示在此机器上的序列号。
雪花算法的64位long型数字由以下几个部分组成:
符号位(1 bit):由于long型数字是有符号的,而雪花算法生成的ID必须是正数,所以符号位固定为0。
时间戳部分(41 bit):记录时间戳,精确到毫秒级别,可以使用当前时间减去一个固定的起始时间,得到一个相对时间戳,从而可以使用41位二进制数表示该时间戳可支持的时间范围为约69年。
机器ID部分(10 bit):可以配置多台机器,每台机器都分配一个唯一的ID,用于在分布式环境下防止ID重复。
序列号部分(12 bit):表示在同一毫秒内生成的序列号,可以使用自增来实现,最多支持4096个ID。
2.2.2.JAVA中使用雪花算法
原生JDK中并没有提供雪花算法的实现,在一些常用的ORM框架中支持了使用雪花算法生成主键ID的功能,如hibernate、mybatis-plus。
以使用Hibernate为例:
可以在实体类的主键字段上使用@GenericGenerator注解和@GeneratedValue注解,其中@GenericGenerator注解用来指定主键生成器的名称和类型,@GeneratedValue注解则用来指定主键生成策略和生成器的名称,例如:
@Entity @Table(name = "user") public class User { @Id @GenericGenerator(name = "snowflake", strategy = "com.xxx.snowflake.SnowflakeIdGenerator") @GeneratedValue(generator = "snowflake") @Column(name = "id") private Long id; // ... }
以mybatis-plus为例:
配置主键生成器为雪花算法的生成器
mybatis-plus:
global-config:
db-config:
id-type: ASSIGN_ID
key-generator: com.baomidou.mybatisplus.core.incrementer.SnowflakeKeyGenerator
然后给主件指定使用所配置的主键生成器
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; @Data public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private Integer age; // 其他字段省略... }