如何去除JDBC PreparedStatement批处理中的重复更新操作?
解决JDBC批处理重复更新的去重方案
嘿,这个问题我之前做批处理优化的时候碰到过!核心痛点就是重复的更新请求白白浪费数据库资源,既然你没法修改填充批处理的算法,那咱们可以从输入预处理或者拦截批处理添加操作这两个方向入手,给你分享两个实用的方案:
方案1:先给roas列表去重,再交给原算法处理
这是最直接的思路——既然原算法是按roas列表生成批处理,那我们先把列表里重复的条目干掉,原算法处理的就是干净的数据了。这里分两种情况:
情况A:只需要保留第一次出现的更新
如果同一个(asn, prefix, max_length)组合的更新,不管后续的isWhitelist/isFilter值如何,执行一次就够,那用LinkedHashMap来做去重(还能保留原来的顺序):
首先得写个用来判断重复的Key类,把三个更新条件包起来:
class RoaUpdateKey { private final long asn; private final Object prefix; private final int maxLength; public RoaUpdateKey(long asn, Object prefix, int maxLength) { this.asn = asn; this.prefix = prefix; this.maxLength = maxLength; } // 一定要重写equals和hashCode,不然Map认不出相同的组合 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RoaUpdateKey that = (RoaUpdateKey) o; return asn == that.asn && maxLength == that.maxLength && Objects.equals(prefix, that.prefix); } @Override public int hashCode() { return Objects.hash(asn, prefix, maxLength); } }
然后处理roas列表:
// 用LinkedHashMap保证顺序,putIfAbsent只保留第一次出现的条目 Map<RoaUpdateKey, Roa> uniqueRoasMap = new LinkedHashMap<>(); for (Roa roa : roas) { RoaUpdateKey key = new RoaUpdateKey(roa.getAsn(), roa.getPrefix(), roa.getMax_length()); uniqueRoasMap.putIfAbsent(key, roa); } // 转成去重后的列表,交给原算法处理 List<Roa> uniqueRoas = new ArrayList<>(uniqueRoasMap.values());
情况B:需要保留最后一次的更新(字段值有变化)
如果同一个更新条件的后续条目里,isWhitelist或isFilter是最新的业务值,那直接用put覆盖之前的条目就行:
Map<RoaUpdateKey, Roa> uniqueRoasMap = new LinkedHashMap<>(); for (Roa roa : roas) { RoaUpdateKey key = new RoaUpdateKey(roa.getAsn(), roa.getPrefix(), roa.getMax_length()); uniqueRoasMap.put(key, roa); // 后面的条目会覆盖前面的,保留最新的 } List<Roa> uniqueRoas = new ArrayList<>(uniqueRoasMap.values());
方案2:给PreparedStatement加个“去重代理”
如果连roas列表的预处理都做不了,那咱们可以给原PreparedStatement套个壳,拦截addBatch()操作,判断当前参数对应的更新条件是不是已经加过批处理,没加过才真正调用原方法:
class DeduplicatingPreparedStatement implements PreparedStatement { private final PreparedStatement delegate; private final Set<RoaUpdateKey> processedKeys = new HashSet<>(); // 用来临时存储当前设置的三个更新条件参数 private long currentAsn; private Object currentPrefix; private int currentMaxLength; public DeduplicatingPreparedStatement(PreparedStatement delegate) { this.delegate = delegate; } // 重写setXXX方法,记录我们需要的三个参数 @Override public void setLong(int parameterIndex, long x) throws SQLException { if (parameterIndex == 1) currentAsn = x; delegate.setLong(parameterIndex, x); } @Override public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { if (parameterIndex == 2) currentPrefix = x; delegate.setObject(parameterIndex, x, targetSqlType); } @Override public void setInt(int parameterIndex, int x) throws SQLException { if (parameterIndex == 3) currentMaxLength = x; delegate.setInt(parameterIndex, x); } // 其他setXXX方法直接委托给原PreparedStatement就行,这里只列了用到的 @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { delegate.setBoolean(parameterIndex, x); } // 核心:拦截addBatch,去重后再执行 @Override public void addBatch() throws SQLException { RoaUpdateKey key = new RoaUpdateKey(currentAsn, currentPrefix, currentMaxLength); // HashSet的add方法返回true表示之前没有这个key,才执行原addBatch if (processedKeys.add(key)) { delegate.addBatch(); } } // 注意:PreparedStatement还有很多其他方法,比如executeBatch、close等,都需要委托给delegate,你可以用IDE自动生成这些方法,或者用CGLIB这类动态代理框架简化代码 }
然后替换原PreparedStatement的创建逻辑:
String updateFilter = "UPDATE payload_roas SET filtered = TRUE WHERE asn = ? AND prefix= ? AND max_length = ?"; PreparedStatement originalPs = connection.prepareStatement(updateFilter); // 套上去重代理 PreparedStatement ps = new DeduplicatingPreparedStatement(originalPs); // 原来的填充算法完全不用改,直接用这个ps就行
额外提个性能优化点
除了去重,记得检查你的JDBC驱动配置,比如MySQL的话,一定要开启rewriteBatchedStatements=true,这个参数能把多条批处理语句合并成一条,性能提升非常明显!另外也可以考虑分批次执行(比如每1000条执行一次executeBatch再clearBatch),避免内存占用过高。
内容的提问来源于stack exchange,提问作者RefiPeretz
相关产品推荐
相关产品推荐

