You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Redisson设置HashMap键TTL遇内存溢出及过期失效问题

Redisson RMapCache高并发插入下过期失效&OOM问题排查与解决

问题场景

使用Redisson的RMapCache为HashMap键设置15分钟TTL,常规场景功能正常,但30分钟内插入500万条数据时,数据未按预期过期,AWS ElasticCache内存使用率快速拉满,触发RedisOutOfMemoryException。

实现代码

RMapCache<String, Object> map = getConnection().getMapCache(mapName);
map.put(key, obj, 15, TimeUnit.MINUTES);

异常信息

org.redisson.client.RedisOutOfMemoryException: command not allowed when used memory > 'maxmemory'. script: 8002ee0f772fdaac4335bffe445fb53aef8dcd57, on @user_script:1.. channel: [id: 0x17227def, L:/10.224.59.44:59436 - R:10.224.57.111/10.224.57.111:6379] data: CommandData [promise=java.util.concurrent.CompletableFuture@120fc639[Not completed, 1 dependents], command=(EVAL), params=[local insertable = false; local v = redis.call('hget', KEYS[1], ARGV[5]); if v == false then insertable = true; else local t, val = struct.unpack('dLc0', v); local expireDate = 92233720368547758; local expireDateScore = redis.call('zscore', KEYS[2], ARGV[5]); if expireDateScore ~= false then expireDate = tonumber(expireDateScore) end; if t ~= 0 then local expireIdle = redis.call('zscore', KEYS[3], ARGV[5]); if expireIdle ~= false then expireDate = math.min(expireDate, tonumber(expireIdle)) end; end; if expireDate <= tonumber(ARGV[1]) then insertable = true; end; end; if tonumber(ARGV[2]) > 0 then redis.call('zadd', KEYS[2], ARGV[2], ARGV[5]); else redis.call('zrem', KEYS[2], ARGV[5]); end; if tonumber(ARGV[3]) > 0 then redis.call('zadd', KEYS[3], ARGV[3], ARGV[5]); else redis.call('zrem', KEYS[3], ARGV[5]); end; local maxSize = tonumber(redis.call('hget', KEYS[8], 'max-size')); if maxSize ~= nil and maxSize ~= 0 then     local currentTime = tonumber(ARGV[1]);     local lastAccessTimeSe..., 8, cid-phone-qa, redisson__timeout__set:{cid-phone-qa}, redisson__idle__set:{cid-phone-qa}, redisson_map_cache_created:{cid-phone-qa}, redisson_map_cache_updated:{cid-phone-qa}, redisson__map_cache__last_access__set:{cid-phone-qa}, redisson_map_cache_removed:{cid-phone-qa}, {cid-phone-qa}:redisson_options, ...], codec=org.redisson.codec.MarshallingCodec]
    at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:361) ~[redisson-3.18.1.jar!/:3.18.1]
    at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:205) ~[redisson-3.18.1.jar!/:3.18.1]
    at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:144) ~[redisson-3.18.1.jar!/:3.18.1]
    at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:120) ~[redisson-3.18.1.jar!/:3.18.1]
    at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:501) ~[netty-codec-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366) ~[netty-codec-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276) ~[netty-codec-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714) ~[netty-transport-4.1.52.Final.jar!/:4.1.52.Final]

问题原因

  1. Redisson过期清理机制滞后:RMapCache依赖后台定时任务清理过期键,默认清理间隔较长(如1分钟),高并发插入时,清理速度跟不上数据写入速度,导致过期数据堆积在内存中。
  2. Redis内存淘汰策略不合理:AWS ElasticCache默认的maxmemory-policy可能未配置为针对带TTL数据的淘汰规则(如volatile-lru),导致内存满时无法自动优先淘汰过期数据。
  3. 单条插入的性能开销:循环调用put单条插入,产生大量Redis命令,占用过多资源,进一步挤占清理任务的执行时间。
  4. Redisson版本潜在问题:使用的3.18.1版本可能存在过期清理相关的bug,导致部分过期数据未被及时识别和删除。

解决方案

  • 调整Redisson清理间隔:在Redisson配置中缩短mapCleanupInterval(默认60000ms),比如设置为10000ms,让后台清理任务更频繁执行。示例配置:
    redisson:
      singleServerConfig:
        address: "redis://your-elasticache-url:6379"
      mapConfig:
        cid-phone-qa:
          mapCleanupInterval: 10000
    
  • 配置Redis内存淘汰策略:在AWS ElasticCache控制台修改maxmemory-policy为volatile-lru,确保内存不足时优先淘汰带TTL的过期/最近最少使用数据,避免OOM。
  • 批量插入优化:使用RMapCache.putAll方法批量插入数据,减少Redis命令次数,降低服务器负载,给清理任务留出执行时间。示例代码:
    Map<String, Object> batchData = new HashMap<>();
    // 批量添加数据到batchData
    map.putAll(batchData, 15, TimeUnit.MINUTES);
    
  • 升级Redisson版本:升级到Redisson 3.20+版本,该版本修复了多个过期清理相关的bug,提升了高并发场景下的稳定性。
  • 拆分数据分片:将500万条数据拆分到多个RMapCache实例中,分散单Map的内存压力和清理负担。

内容的提问来源于stack exchange,提问作者bigDataArtist

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 22:25:04