Ignite 2.13.x并发更新缓存键报并发修改异常问题咨询
问题场景
业务中使用Ignite 2.13.x版本维护集群缓存,部分缓存键值存在并发更新操作,同时有并行的查询读取请求,运行时抛出如下报错:
java.sql.SQLException: Failed to update some keys because they had been modified concurrently [keys=[ROWKEY [idHash=628930863, hash=1637498765, rowkey=6, id=765436]]] at org.apache.ignite.internal.jdbc.thin.JdbcThinConnection.sendRequest(JdbcThinConnection.java:1009) at org.apache.ignite.internal.jdbc.thin.JdbcThinStatement.execute0(JdbcThinStatement.java:234) at org.apache.ignite.internal.jdbc.thin.JdbcThinPreparedStatement.executeWithArguments(JdbcThinPreparedStatement.java:252) at org.apache.ignite.internal.jdbc.thin.JdbcThinPreparedStatement.executeUpdate(JdbcThinPreparedStatement.java:96) at sun.reflect.GeneratedMethodAccessor56.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.apache.tomcat.jdbc.pool.StatementFacade$StatementProxy.invoke(StatementFacade.java:118) at com.sun.proxy.$Proxy16.executeUpdate(Unknown Source)
服务端配置
<bean class="org.apache.ignite.configuration.IgniteConfiguration" id="igniteConfiguration"> <property name="cacheKeyConfiguration"> <list> <bean class="org.apache.ignite.cache.CacheKeyConfiguration"> <property name="typeName" value="ROWKEY" /> <property name="affinityKeyFieldName" value="rowkey" /> </bean> </list> </property> <property name="clientMode" value="false" /> <property name="gridName" value="6d-ignite-grid"></property> <property name="cacheConfiguration"> <list> <bean class="org.apache.ignite.configuration.CacheConfiguration"> <property name="name" value="persons"></property> <property name="cacheMode" value="PARTITIONED" /> <property name="backups" value="0" /> <property name="onheapCacheEnabled" value="false" /> <property name="statisticsEnabled" value="true" /> <property name="memoryPolicyName" value="Default_Region" /> <property name="copyOnRead" value="false" /> <property name="writeSynchronizationMode" value="FULL_SYNC" /> <property name="affinity"> <bean class="org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction"> <property name="excludeNeighbors" value="true" /> <property name="partitions" value="10" /> <property name="affinityBackupFilter"> <bean class="org.apache.ignite.cache.affinity.rendezvous.ClusterNodeAttributeAffinityBackupFilter"> <constructor-arg> <array value-type="java.lang.String"> <value>AVAILABILITY_ZONE</value> </array> </constructor-arg> </bean> </property> </bean> </property> <property name="queryEntities"> <list> <bean class="org.apache.ignite.cache.QueryEntity"> <property name="keyType" value="ROWKEY"></property> <property name="valueType" value="PERSONS"></property> <property name="fields"> <map> <entry key="rowkey" value="java.lang.Long"></entry> <entry key="id" value="java.lang.Long"></entry> <entry key="name" value="java.lang.String"></entry> <entry key="birthday" value="java.lang.String"></entry> <entry key="gender" value="java.lang.String"></entry> <entry key="amount" value="java.lang.Long"></entry> <entry key="salary" value="java.lang.Long"></entry> <entry key="orgid" value="java.lang.String"></entry> </map> </property> <property name="keyFields"> <set> <value>rowkey</value> <value>id</value> </set> </property> </bean> </list> </property> </bean> </list> </property> <property name="includeEventTypes"> <list> <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_STARTED" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FINISHED" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FAILED" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_TIMEDOUT" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_SESSION_ATTR_SET" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_REDUCED" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_PUT" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_READ" /> <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_REMOVED" /> </list> </property> <property name="peerClassLoadingEnabled" value="true"></property> <property name="discoverySpi"> <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi"> <property name="ipFinder"> <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder"> <property name="addresses"> <list> <value>127.0.0.1:47500..47509</value> </list> </property> </bean> </property> </bean> </property> <property name="memoryConfiguration"> <bean class="org.apache.ignite.configuration.MemoryConfiguration"> <property name="defaultMemoryPolicyName" value="Default_Region" /> <property name="pageSize" value="4096" /> <property name="systemCacheInitialSize" value="#{40 * 1024 * 1024}" /> <property name="systemCacheMaxSize" value="#{40 * 1024 * 1024}" /> <property name="memoryPolicies"> <list> <bean class="org.apache.ignite.configuration.MemoryPolicyConfiguration"> <property name="name" value="Default_Region" /> <property name="initialSize" value="#{20 * 1024 * 1024}" /> <property name="maxSize" value="#{20 * 1024 * 1024}" /> <property name="pageEvictionMode" value="RANDOM_2_LRU" /> <property name="evictionThreshold" value="0.6" /> <property name="metricsEnabled" value="true" /> </bean> </list> </property> </bean> </property> </bean>
问题原因与修复方案
该报错本质是Ignite乐观并发控制校验失败:多个并发事务同时更新同一个键时,后提交的事务检测到键的版本已经被其他事务修改,为避免数据覆盖直接抛出异常,结合当前配置还有几个放大冲突概率的问题:
- 缓存设置
backups=0无副本,节点负载波动、网络延迟都可能触发版本校验误判,生产环境不推荐该配置 - 主键配置不匹配:QueryEntity中定义
rowkey和id为联合键字段,但亲和性路由仅配置了rowkey,会出现同一条逻辑数据路由到不同分区的问题,加剧写入冲突 copyOnRead=false配置下,读写操作共享内存引用,高并发场景可能读到未提交的中间版本,触发不必要的版本校验失败
按以下步骤修复即可:
- 增加重试容错
捕获该异常后做指数退避重试,JDBC更新逻辑配置3-5次重试即可覆盖绝大多数偶发并发冲突,参考实现:int maxRetry = 5; for (int i = 0; i <= maxRetry; i++) { try (PreparedStatement ps = conn.prepareStatement(updateSql)) { // 填充SQL参数 ps.executeUpdate(); break; } catch (SQLException e) { if (i == maxRetry || !e.getMessage().contains("modified concurrently")) { throw e; } Thread.sleep(10L * (i + 1)); } } - 修正配置项
- 将persons缓存的
backups调整为至少1,保证副本冗余,降低节点波动带来的异常 - 统一主键路由规则:如果确实使用
rowkey+id作为联合主键,自定义ROWKEY类实现正确的equals()和hashCode()方法,亲和性字段选择业务上区分度最高的字段即可,避免同一条数据被路由到多个分区 - 高并发读写场景将
copyOnRead设为true,关闭内存引用共享,避免读写互相干扰,该配置带来的性能损耗远低于冲突重试的开销
- 将persons缓存的
- 按需调整事务隔离级别
如果业务不需要强一致的串行化隔离,可以在JDBC连接串添加参数TRANSACTION_SERIALIZABLE=READ_COMMITTED,降低乐观锁校验严格度,减少非必要冲突,资金类强一致场景不建议调整该参数。
内容的提问来源于stack exchange,提问作者Ajay
相关产品推荐
相关产品推荐

