Spring 5+H2内存库实现原子性插入:仅新增不存在的数据
实现H2数据库原子性"不存在则插入"操作(类似ConcurrentHashMap.putIfAbsent)
你遇到的H2不支持PL风格代码块的问题很常见,不过我们可以通过H2原生支持的SQL语法或者Spring事务特性来实现你要的原子操作。下面给你两种可行的方案,都能配合@Transactional注解保证操作的原子性:
方案一:使用H2的MERGE语句(推荐)
H2支持标准的MERGE语法,这个语句本身就是原子性的,能直接实现"不存在则插入"的逻辑,不需要额外的PL块。结合NamedParameterJdbcTemplate(比普通JdbcTemplate更适合带命名参数的场景),代码如下:
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @Transactional public void create(String city, Double temperature) { // H2的MERGE语法:当city不存在时执行插入,存在则不做任何操作 String mergeSql = """ MERGE INTO weather (city, temperature, time_stamp) KEY (city) VALUES (:city, :temperature, :time_stamp) """; MapSqlParameterSource params = new MapSqlParameterSource() .addValue("city", city) .addValue("temperature", temperature) .addValue("time_stamp", new Date()); namedParameterJdbcTemplate.update(mergeSql, params); System.out.println("操作完成:若城市不存在则已插入记录"); }
说明:
KEY (city)指定了判断记录是否存在的唯一键(这里假设city是表的唯一约束字段,最好给weather表的city列加UNIQUE约束,这样MERGE的判断更可靠)。@Transactional注解在这里可以保证如果操作过程中出现异常,会自动回滚,同时也能确保整个MERGE操作在事务上下文里执行(虽然MERGE本身是原子的,但事务能和其他操作保持一致性)。
方案二:先查询再插入(需注意事务隔离级别)
如果你更倾向于用Java逻辑控制,也可以先查询是否存在记录,不存在再插入,但必须通过事务隔离级别避免并发场景下的竞态条件:
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @Transactional(isolation = Isolation.SERIALIZABLE) // 用最高隔离级别防止并发问题 public void create(String city, Double temperature) { // 先查询是否存在该城市的记录 String checkSql = "SELECT COUNT(*) FROM weather WHERE city = :city"; Long count = namedParameterJdbcTemplate.queryForObject( checkSql, new MapSqlParameterSource("city", city), Long.class ); if (count == 0) { String insertSql = """ INSERT INTO weather (city, temperature, time_stamp) VALUES (:city, :temperature, :time_stamp) """; MapSqlParameterSource params = new MapSqlParameterSource() .addValue("city", city) .addValue("temperature", temperature) .addValue("time_stamp", new Date()); namedParameterJdbcTemplate.update(insertSql, params); System.out.println("成功插入新的天气记录"); } else { System.out.println("该城市的记录已存在,跳过插入"); } }
说明:
- 必须设置
isolation = Isolation.SERIALIZABLE,否则在高并发场景下,可能出现两个线程同时查询到记录不存在,然后都执行插入的情况,破坏原子性。 - 这种方案比MERGE多了一次数据库查询,性能略差,所以优先推荐方案一。
关键注意点
- 确保
weather表的city字段有UNIQUE约束,这是保证"不存在则插入"逻辑可靠的基础,即使出现极端并发情况,数据库层面也会阻止重复插入。 - 使用
NamedParameterJdbcTemplate比普通JdbcTemplate更适合处理命名参数,代码可读性更高,也避免了索引参数的错误。
内容的提问来源于stack exchange,提问作者user3636486
相关产品推荐
相关产品推荐

