Hibernate 主键生成策略——Duplicate entry '1024' for key 'PRIMARY'

日常搬砖踩坑系列——Hibernate主键生成策略,主键冲突

项目开发完毕,前后端接口联调;前端童鞋反应新增接口偶尔会报错,经过查看后端服务日志:java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '1024' for key 'PRIMARY',明显是写入数据主键冲突,一个新增接口并且数据表的主键是自增的,怎么会主键冲突呢?

还原场景

接口联调,基本是在dev环境,有些时候为了方便开发人员也会本地启动服务连接同一个数据库;前端在切换api地址测试接口时会报错。

分析原因

因为主键是自增的,并且新增接口没有指定id(依靠数据库自增),居然会出现主键冲突错误,难道新增时id被指定了而且是开发人员不知情,貌似找到了原因,因为项目中实体类指定了主键生成策略;代码如下:

@Entity
@Table(name = "user")
public class User {
    @Id
    @GenericGenerator(name = "autoId", strategy = "increment")
    @GeneratedValue(generator = "autoId")
    private Integer id;
    private String name;
}

验证:将项目SQL进行日志输出(jpa.show-sql=true),strategy = "increment"

Hibernate: select max(id) from user
Hibernate: insert into user (name, id) values (?, ?)

果然新增时指定了主键id,并且select max(id) from user

google了一番,发现当主键生成策略指定为“increment”,插入数据的时候hibernate会通过自己维护的主键给主键赋值,相当于hibernate实例就维护一个计数器作为主键,所以在多个实例(集群)运行的时候不能使用这个生成策略;找到问题的根源了,解决办法把“increment”改为“native”或“identity”,推荐“native”,不需要hibernate维护主键id,依靠数据库完成这个任务,问题得以解决。

验证:将项目SQL进行日志输出(jpa.show-sql=true),strategy = "native"

Hibernate: insert into user (name) values (?)

当strategy = "increment",第一次会将表中最大id查询出来,hibernate维护这个id(并且多个开发启动多个服务实例各自维护id),不依靠底层数据库才导致主键冲突。

拓展知识

那么主键生成策略多有那些呢?

GeneratedValue
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface GeneratedValue {
    // 生成策略
    GenerationType strategy() default AUTO;
    // 生成器名称
    String generator() default "";
}

public enum GenerationType { 
    // 使用一个特定的数据库表格来保存主键
    TABLE, 
    // 根据底层数据库的序列来生成主键,条件是数据库支持序列(Oracle)。
    SEQUENCE, 
    // 主键由数据库自动生成(主要是自动增长型,MySQL、SQL Server)
    IDENTITY, 
    // 主键由程序控制
    AUTO
}

  • TableGenerator 表生成器, GeneratedValue的strategy为GenerationType.TABLE

将当前主键的值单独保存到数据库的一张表里去,主键的值每次都是从该表中查询获得,适用于任何数据库,不必担心兼容问题

@Repeatable(TableGenerators.class)
@Target({TYPE, METHOD, FIELD}) 
@Retention(RUNTIME)
public @interface TableGenerator {
    // 属性表示该生成器的名称,它被引用在@GeneratedValue中设置的“generator”值中
    String name();
    // 主键保存到数据库的表名
    String table() default "";
    String catalog() default "";
    String schema() default "";
    // 表里用来保存主键名字的字段
    String pkColumnName() default "";
    // 表里用来保存主键值的字段
    String valueColumnName() default "";
    // 表里名字字段对应的值
    String pkColumnValue() default "";
    int initialValue() default 0;
    // //自动增长,设置为1
    int allocationSize() default 50;
    UniqueConstraint[] uniqueConstraints() default {};
    Index[] indexes() default {};
}

@Data
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(generator="tableGenerator",strategy = GenerationType.TABLE)
    @TableGenerator(name="tableGenerator",
            table = "id_table",
            pkColumnName = "id_name",
            valueColumnName = "id_value",
            pkColumnValue = "user_id",
            initialValue = 1,
            allocationSize = 1)
    private Integer id;
    private String name;
}

需要id_table表存放主键

id id_name id_value
1 user_id 1

新增数据时,需要从id_table将id_value查询出来,写入user表,更新id_table表id_value,流程日志如下:

Hibernate: select tbl.id_value from id_table tbl where tbl.id_name=? for update
Hibernate: update id_table set id_value=?  where id_value=? and id_name=?
Hibernate: insert into user (name, id) values (?, ?)
  • SequenceGenerator 序列生成器,条件是数据库支持序列(Oracle);GeneratedValue的strategy为GenerationType.SEQUENCE
@Target({TYPE, METHOD, FIELD})   
@Retention(RUNTIME)  
public @interface SequenceGenerator {  
    // 属性表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中
    String name();  
    // 属性表示生成策略用到的数据库序列名称
    String sequenceName() default ""; 
    // 表示主键初识值,默认为0 
    int initialValue() default 0;  
    // 表示每次主键值增加的大小,例如设置成1,则表示每次创建新记录后自动加1,默认为50
    int allocationSize() default 50;  
}  

// 条件是数据库支持序列(Oracle)
@Id  
@GeneratedValue(strategy =GenerationType.SEQUENCE,generator="sequenceGenerator")  
@SequenceGenerator(name="sequenceGenerator", sequenceName="sequence_name")  
  • IDENTITY 主键则由数据库自动维护,使用起来很简单
@Id  
@GeneratedValue(strategy = GenerationType.IDENTITY)
// 等价于
@Id
@GenericGenerator(name = "autoId", strategy = "native")
@GeneratedValue(generator = "autoId")
// 或
@Id
@GenericGenerator(name = "autoId", strategy = "identity")
@GeneratedValue(generator = "autoId")
  • AUTO 默认的配置。如果不指定主键生成策略,默认为AUTO,需要配合GenericGenerators使用
// 自定义主键生成策略
@Target({PACKAGE, TYPE, METHOD, FIELD})
@Retention(RUNTIME)
@Repeatable(GenericGenerators.class)
public @interface GenericGenerator {
    // 属性表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中
	String name();
    // 属性指定具体生成器的类名
	String strategy();
    // parameters得到strategy指定的具体生成器所用到的参数
	Parameter[] parameters() default {};
}

通过DefaultIdentifierGeneratorFactory实现

public DefaultIdentifierGeneratorFactory() {
        // 发现此处并没有native,
		register( "uuid2", UUIDGenerator.class );
		register( "guid", GUIDGenerator.class );			// can be done with UUIDGenerator + strategy
		register( "uuid", UUIDHexGenerator.class );			// "deprecated" for new use
		register( "uuid.hex", UUIDHexGenerator.class ); 	// uuid.hex is deprecated
		register( "assigned", Assigned.class );
		register( "identity", IdentityGenerator.class );
		register( "select", SelectGenerator.class );
		register( "sequence", SequenceStyleGenerator.class );
		register( "seqhilo", SequenceHiLoGenerator.class );
		register( "increment", IncrementGenerator.class );
		register( "foreign", ForeignGenerator.class );
		register( "sequence-identity", SequenceIdentityGenerator.class );
		register( "enhanced-sequence", SequenceStyleGenerator.class );
		register( "enhanced-table", TableGenerator.class );
	}

@Override
public Class getIdentifierGeneratorClass(String strategy) {
	if ( "hilo".equals( strategy ) ) {
		throw new UnsupportedOperationException( "Support for 'hilo' generator has been removed" );
	}
    // 在这里
	String resolvedStrategy = "native".equals( strategy ) ?
			getDialect().getNativeIdentifierGeneratorStrategy() : strategy;

	Class generatorClass = generatorStrategyToClassNameMap.get( resolvedStrategy );

常用的生成策略

  • increment 插入数据的时候hibernate会给主键添加一个自增的主键,但是一个hibernate实例就维护一个计数器,所以在多个实例运行的时候不能使用这个方法,查看IncrementGenerator实现
public class IncrementGenerator implements IdentifierGenerator, Configurable {
	private String sql;
    private IntegralDataTypeHolder previousValueHolder;

    // 同步方法,保证线程安全
    @Override
	public synchronized Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
        // 第一次sql!=null,select max
		if ( sql != null ) {
			initializePreviousValueHolder( session );
		}
        // 获取id并自增
		return previousValueHolder.makeValueThenIncrement();
	}
	@Override
	public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
		// 此处与日志打印的相吻合
		sql = "select max(" + column + ") from " + buf.toString();
	}
    private void initializePreviousValueHolder(SharedSessionContractImplementor session) {
		previousValueHolder = IdentifierGeneratorHelper.getIntegralDataTypeHolder( returnClass );

		final boolean debugEnabled = LOG.isDebugEnabled();
		if ( debugEnabled ) {
			LOG.debugf( "Fetching initial value: %s", sql );
		}
		try {
			PreparedStatement st = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql );
			try {
				ResultSet rs = session.getJdbcCoordinator().getResultSetReturn().extract( st );
				try {
					if ( rs.next() ) {
						previousValueHolder.initialize( rs, 0L ).increment();
					}
					else {
						previousValueHolder.initialize( 1L );
					}
                    // generate 不在select max 
					sql = null;
					if ( debugEnabled ) {
						LOG.debugf( "First free id: %s", previousValueHolder.makeValue() );
					}
	}
}

// 处理维护的id
public final class IdentifierGeneratorHelper {
	public Number makeValueThenIncrement() {
		final Number result = makeValue();
		value = value.add( BigInteger.ONE );
		return result;
	}    
}

  • identity 使用SQL Server 和 MySQL 的自增字段,这个方法不能放到 Oracle 中,Oracle 不支持自增字段,要设定sequence(MySQL 和 SQL Server 中很常用)
  • sequence 调用数据库的sequence来生成主键,要设定序列名,不然hibernate无法找到(Oracle 中常用)
  • native 对于 oracle 采用 Sequence(序列),对于MySQL 和 SQL Server 采用identity(自增主键),native就是将主键的生成工作交由数据库完成,hibernate不管(很常用、推荐)

当把@GenericGenerator注释或去掉,把@GeneratedValue(strategy = GenerationType.IDENTITY)再次测试,日志没有打印select max(id) from user,不需要维护id,这是另一种解决方案。

总结

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '1024' for key 'PRIMARY' 出现,是因为主键生成策略strategy = "increment",

  • strategy = "increment"
    • 优点,使用起来比较方便,跨数据库,不许底层数据库支持自增,由hibernate实现自增
    • 缺点,hibernate实现自增,即同一个JVM内没有问题,如果服务是集群模式(多JVM),就会出现主键冲突问题
@Id
@GenericGenerator(name = "autoId", strategy = "increment")
@GeneratedValue(generator = "autoId")
// 用下面即可
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
  • GenerationType.TABLE同样可已跨数据库,GenerationType.SEQUENCE主要用于oralce、PostgerSQL支持sequence机制的数据库,GenerationType.IDENTITY主要用MySQL、SQL Server等支持主键自增的数据库

Java的生态太强大,知道怎么用的同时,还是知道其实现原理。

原文地址:https://www.cnblogs.com/sunjingwu/p/11700403.html