日常搬砖踩坑系列——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