一、背景

redis是缓存界的扛把子,而redisson又是java 链接redis 的一哥,每个项目基本都会用到redisson。
它的功能丰富让人羡慕,但是功能丰富极有可能导致使用姿势不正常,就爆发线上问题,这里说两个踩得雷以及解决方法。

二、问题、分析和方案

1、数据结合在高并发情况下的情况:RMap 和RSet 数据不一致问题

操作Rmap: 高并发时候,如果两个线程同时获取Rmap的数据,那原始数据进行操作后,最终结果是延迟放回去数据那个线程对应的结果。
原因:每个线程获取到的都是数据最原始的状态,并没有版本控制一说。

解决方案:
使用时候,根据场景评判是否需要加redisson的锁

2、 反序列化问题

操作对象:如果直接给Redis 中存放对象时候,一定要注意,在类有变化时候,如果使用FstCodec进行对象序列化,反序列化回来的对象会报错,
原因:redisson FstCodec 需要判定类模版

解决方案: 
使用jackson序列化, 或者存入对象时候,先转成json格式进行存储

3、 阻塞队列内存泄露OOM问题

操作对象:在 3.12.5 以下版本,使用 Redisson 的阻塞队列进行take操作,假设如果一直获取不到数据,会存在内存泄漏问题;
原因:使用阻塞队列时,由于连接长时间阻塞无数据,会导致当前连接的会一直进行ping探活,会一直新建CommandData,队列中数据CommandData无限增长最终出现内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 //因为一直take,就会触发队列拿数据
public class PingConnectionHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
RedisConnection connection = RedisConnection.getFrom(ctx.channel());
connection.getConnectionPromise().onComplete((res, e) -> {
if (e == null) {
sendPing(ctx);
}
});
ctx.fireChannelActive();
}

//启动定时,不停的执行sendping 方法,
protected void sendPing(final ChannelHandlerContext ctx) {
final RedisConnection connection = RedisConnection.getFrom(ctx.channel());
final RFuture<String> future = connection.async(StringCodec.INSTANCE, RedisCommands.PING);

config.getTimer().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
CommandData<?, ?> commandData = connection.getCurrentCommand();
if ((commandData == null || !commandData.isBlockingCommand())
&& (future.cancel(false) || !future.isSuccess())) {
ctx.channel().close();
log.debug("channel: {} closed due to PING response timeout set in {} ms", ctx.channel(), config.getPingConnectionInterval());
} else {
sendPing(ctx);
}
}
}, config.getPingConnectionInterval(), TimeUnit.MILLISECONDS);
}
}

// 因为上面会不停的执行,所以到了这块,就会不停的给queue中新增对象

public class ChannelDuplexHandler extends ChannelInboundHandlerAdapter implements ChannelOutboundHandler
//写出时候,如果没有数据,则会在queue中新增一个QueueCommandHolder对象
public class CommandsQueue extends ChannelDuplexHandler {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof QueueCommand) {
QueueCommand data = (QueueCommand) msg;
QueueCommandHolder holder = queue.peek();
if (holder != null && holder.getCommand() == data) {
super.write(ctx, msg, promise);
} else {
queue.add(new QueueCommandHolder(data, promise));
sendData(ctx.channel());
}
} else {
super.write(ctx, msg, promise);
}
}
}

3、 RMap存储对象多导致OOM问题

操作CacheMap :如果使用cacheMap,如果放入太多的key,会导致线上OOM。
原因:java服务会启动过多的Task来定时执行检测Key的过期,
如下代码中的:EvictionTask 及根据不同的key 来起一个定时,如果key 多了,对象也会很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public <K, V> RMapCache<K, V> getMapCache(String name, Codec codec) {
return new RedissonMapCache<K, V>(codec, evictionScheduler, commandExecutor, name, this, null, null);
}

public RedissonMapCache(EvictionScheduler evictionScheduler, CommandAsyncExecutor commandExecutor,
String name, RedissonClient redisson, MapCacheOptions<K, V> options, WriteBehindService writeBehindService) {
super(commandExecutor, name, redisson, options, writeBehindService);
if (evictionScheduler != null) {
evictionScheduler.schedule(getRawName(), getTimeoutSetName(), getIdleSetName(), getExpiredChannelName(), getLastAccessTimeSetName(), options);
}
this.evictionScheduler = evictionScheduler;
this.publishCommand = commandExecutor.getConnectionManager().getSubscribeService().getPublishCommand();
}

public void schedule(String name, String timeoutSetName, String maxIdleSetName,
String expiredChannelName, String lastAccessTimeSetName, MapCacheOptions<?, ?> options) {
boolean removeEmpty = false;
if (options != null) {
removeEmpty = options.isRemoveEmptyEvictionTask();
}

EvictionTask task = new MapCacheEvictionTask(name, timeoutSetName, maxIdleSetName, expiredChannelName, lastAccessTimeSetName,
executor, removeEmpty, this);
EvictionTask prevTask = tasks.putIfAbsent(name, task);
if (prevTask == null) {
task.schedule();
}
}

解决方案:

使用redis 的原始map,同时使用expire 来让原始map过期,至于map中的详细数据,则自维护一个 过期策略,如下给出示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   public class OwnLFU <K, V> extends LinkedHashMap<K, V> {
private final int capacity;

public OwnLFU(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
//这里也可以加上自己的逻辑,比如,如果往该LFU里面存入的是 时间戳之类的,就对key进行比对,返回是否删除
// if((Long)eldest.getKey() > System.currentTimeMillis()){
// return true;
// }
return size() > capacity;
}

}