一、前提

最近在使用分布式锁redisson时遇到一个线上问题:发现是subscriptionsPerConnection or subscriptionConnectionPoolSize 的大小不够,需要提高配置才能解决。

二、源码分析

下面对其源码进行分析,才能找到到底是什么逻辑导致问题所在:

1、RedissonLock#lock() 方法

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 尝试获取,如果ttl == null,则表示获取锁成功
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
} // 订阅锁释放事件,并通过await方法阻塞等待锁释放,有效的解决了无效的锁申请浪费资源的问题
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
} // 后面代码忽略
try {
// 无限循环获取锁,直到获取锁成功
// ...
} finally {
// 取消订阅锁释放事件
unsubscribe(future, threadId);
}
}

总结下主要逻辑:

  1. 获取当前线程的线程id;
  2. tryAquire尝试获取锁,并返回ttl
  3. 如果ttl为空,则结束流程;否则进入后续逻辑;
  4. this.subscribe(threadId)订阅当前线程,返回一个RFuture;
  5. 如果在指定时间没有监听到,则会产生如上异常。
  6. 订阅成功后, 通过while(true)循环,一直尝试获取锁
  7. fially代码块,会解除订阅

    所以上述这情况问题应该出现在subscribe()方法中

2、详细看下subscribe()方法

protected RFuture<RedissonLockEntry> subscribe(long threadId) {
// entryName 格式:“id:name”;
// channelName 格式:“redisson_lock__channel:name”;
return pubSub.subscribe(getEntryName(), getChannelName());
}

RedissonLock#pubSub 是在RedissonLock构造函数中初始化的:

public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
// ....
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}

subscribeServiceMasterSlaveConnectionManager的实现中又是通过如下方式构造的

public MasterSlaveConnectionManager(MasterSlaveServersConfig cfg, Config config, UUID id) {
this(config, id);
this.config = cfg; // 初始化
initTimer(cfg);
initSingleEntry();
} protected void initTimer(MasterSlaveServersConfig config) {
int[] timeouts = new int[]{config.getRetryInterval(), config.getTimeout()};
Arrays.sort(timeouts);
int minTimeout = timeouts[0];
if (minTimeout % 100 != 0) {
minTimeout = (minTimeout % 100) / 2;
} else if (minTimeout == 100) {
minTimeout = 50;
} else {
minTimeout = 100;
} timer = new HashedWheelTimer(new DefaultThreadFactory("redisson-timer"), minTimeout, TimeUnit.MILLISECONDS, 1024, false); connectionWatcher = new IdleConnectionWatcher(this, config); // 初始化:其中this就是MasterSlaveConnectionManager实例,config则为MasterSlaveServersConfig实例:
subscribeService = new PublishSubscribeService(this, config);
}

PublishSubscribeService构造函数

private final SemaphorePubSub semaphorePubSub = new SemaphorePubSub(this);
public PublishSubscribeService(ConnectionManager connectionManager, MasterSlaveServersConfig config) {
super();
this.connectionManager = connectionManager;
this.config = config;
for (int i = 0; i < locks.length; i++) {
// 这里初始化了一组信号量,每个信号量的初始值为1
locks[i] = new AsyncSemaphore(1);
}
}

3、回到subscribe()方法主要逻辑还是交给了 LockPubSub#subscribe()里面

private final ConcurrentMap<String, E> entries = new ConcurrentHashMap<>();

public RFuture<E> subscribe(String entryName, String channelName) {
// 从PublishSubscribeService获取对应的信号量。 相同的channelName获取的是同一个信号量
// public AsyncSemaphore getSemaphore(ChannelName channelName) {
// return locks[Math.abs(channelName.hashCode() % locks.length)];
// }
AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName)); AtomicReference<Runnable> listenerHolder = new AtomicReference<Runnable>();
RPromise<E> newPromise = new RedissonPromise<E>() {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return semaphore.remove(listenerHolder.get());
}
}; Runnable listener = new Runnable() { @Override
public void run() {
// 如果存在RedissonLockEntry, 则直接利用已有的监听
E entry = entries.get(entryName);
if (entry != null) {
entry.acquire();
semaphore.release();
entry.getPromise().onComplete(new TransferListener<E>(newPromise));
return;
} E value = createEntry(newPromise);
value.acquire(); E oldValue = entries.putIfAbsent(entryName, value);
if (oldValue != null) {
oldValue.acquire();
semaphore.release();
oldValue.getPromise().onComplete(new TransferListener<E>(newPromise));
return;
} // 创建监听,
RedisPubSubListener<Object> listener = createListener(channelName, value);
// 订阅监听
service.subscribe(LongCodec.INSTANCE, channelName, semaphore, listener);
}
}; // 最终会执行listener.run方法
semaphore.acquire(listener);
listenerHolder.set(listener); return newPromise;
}

AsyncSemaphore#acquire()方法

public void acquire(Runnable listener) {
acquire(listener, 1);
} public void acquire(Runnable listener, int permits) {
boolean run = false; synchronized (this) {
// counter初始化值为1
if (counter < permits) {
// 如果不是第一次执行,则将listener加入到listeners集合中
listeners.add(new Entry(listener, permits));
return;
} else {
counter -= permits;
run = true;
}
} // 第一次执行acquire, 才会执行listener.run()方法
if (run) {
listener.run();
}
}

梳理上述逻辑:

  • 1、从PublishSubscribeService获取对应的信号量, 相同的channelName获取的是同一个信号量
  • 2、如果是第一次请求,则会立马执行listener.run()方法, 否则需要等上个线程获取到该信号量执行完方能执行;
  • 3、如果已经存在RedissonLockEntry, 则利用已经订阅就行
  • 4、如果不存在RedissonLockEntry, 则会创建新的RedissonLockEntry,然后进行。

    从上面代码看,主要逻辑是交给了PublishSubscribeService#subscribe方法

4、PublishSubscribeService#subscribe逻辑如下:

private final ConcurrentMap<ChannelName, PubSubConnectionEntry> name2PubSubConnection = new ConcurrentHashMap<>();
private final Queue<PubSubConnectionEntry> freePubSubConnections = new ConcurrentLinkedQueue<>(); public RFuture<PubSubConnectionEntry> subscribe(Codec codec, String channelName, AsyncSemaphore semaphore, RedisPubSubListener<?>... listeners) {
RPromise<PubSubConnectionEntry> promise = new RedissonPromise<PubSubConnectionEntry>();
// 主要逻辑入口, 这里要主要channelName每次都是新对象, 但内部覆写hashCode+equals。
subscribe(codec, new ChannelName(channelName), promise, PubSubType.SUBSCRIBE, semaphore, listeners);
return promise;
} private void subscribe(Codec codec, ChannelName channelName, RPromise<PubSubConnectionEntry> promise, PubSubType type, AsyncSemaphore lock, RedisPubSubListener<?>... listeners) { PubSubConnectionEntry connEntry = name2PubSubConnection.get(channelName);
if (connEntry != null) {
// 从已有Connection中取,如果存在直接把listeners加入到PubSubConnectionEntry中
addListeners(channelName, promise, type, lock, connEntry, listeners);
return;
} // 没有时,才是最重要的逻辑
freePubSubLock.acquire(new Runnable() { @Override
public void run() {
if (promise.isDone()) {
lock.release();
freePubSubLock.release();
return;
} // 从队列中取头部元素
PubSubConnectionEntry freeEntry = freePubSubConnections.peek();
if (freeEntry == null) {
// 第一次肯定是没有的需要建立
connect(codec, channelName, promise, type, lock, listeners);
return;
} // 如果存在则尝试获取,如果remainFreeAmount小于0则抛出异常终止了。
int remainFreeAmount = freeEntry.tryAcquire();
if (remainFreeAmount == -1) {
throw new IllegalStateException();
} PubSubConnectionEntry oldEntry = name2PubSubConnection.putIfAbsent(channelName, freeEntry);
if (oldEntry != null) {
freeEntry.release();
freePubSubLock.release(); addListeners(channelName, promise, type, lock, oldEntry, listeners);
return;
} // 如果remainFreeAmount=0, 则从队列中移除
if (remainFreeAmount == 0) {
freePubSubConnections.poll();
}
freePubSubLock.release(); // 增加监听
RFuture<Void> subscribeFuture = addListeners(channelName, promise, type, lock, freeEntry, listeners); ChannelFuture future;
if (PubSubType.PSUBSCRIBE == type) {
future = freeEntry.psubscribe(codec, channelName);
} else {
future = freeEntry.subscribe(codec, channelName);
} future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
if (!promise.isDone()) {
subscribeFuture.cancel(false);
}
return;
} connectionManager.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
subscribeFuture.cancel(false);
}
}, config.getTimeout(), TimeUnit.MILLISECONDS);
}
});
} });
} private void connect(Codec codec, ChannelName channelName, RPromise<PubSubConnectionEntry> promise, PubSubType type, AsyncSemaphore lock, RedisPubSubListener<?>... listeners) {
// 根据channelName计算出slot获取PubSubConnection
int slot = connectionManager.calcSlot(channelName.getName());
RFuture<RedisPubSubConnection> connFuture = nextPubSubConnection(slot);
promise.onComplete((res, e) -> {
if (e != null) {
((RPromise<RedisPubSubConnection>) connFuture).tryFailure(e);
}
}); connFuture.onComplete((conn, e) -> {
if (e != null) {
freePubSubLock.release();
lock.release();
promise.tryFailure(e);
return;
} // 这里会从配置中读取subscriptionsPerConnection
PubSubConnectionEntry entry = new PubSubConnectionEntry(conn, config.getSubscriptionsPerConnection());
// 每获取一次,subscriptionsPerConnection就会减直到为0
int remainFreeAmount = entry.tryAcquire(); // 如果旧的存在,则将现有的entry释放,然后将listeners加入到oldEntry中
PubSubConnectionEntry oldEntry = name2PubSubConnection.putIfAbsent(channelName, entry);
if (oldEntry != null) {
releaseSubscribeConnection(slot, entry); freePubSubLock.release(); addListeners(channelName, promise, type, lock, oldEntry, listeners);
return;
} if (remainFreeAmount > 0) {
// 加入到队列中
freePubSubConnections.add(entry);
}
freePubSubLock.release(); RFuture<Void> subscribeFuture = addListeners(channelName, promise, type, lock, entry, listeners); // 这里真正的进行订阅(底层与redis交互)
ChannelFuture future;
if (PubSubType.PSUBSCRIBE == type) {
future = entry.psubscribe(codec, channelName);
} else {
future = entry.subscribe(codec, channelName);
} future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
if (!promise.isDone()) {
subscribeFuture.cancel(false);
}
return;
} connectionManager.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
subscribeFuture.cancel(false);
}
}, config.getTimeout(), TimeUnit.MILLISECONDS);
}
});
});
}

PubSubConnectionEntry#tryAcquire方法, subscriptionsPerConnection代表了每个连接的最大订阅数。当tryAcqcurie的时候会减少这个数量:

 public int tryAcquire() {
while (true) {
int value = subscribedChannelsAmount.get();
if (value == 0) {
return -1;
} if (subscribedChannelsAmount.compareAndSet(value, value - 1)) {
return value - 1;
}
}
}

梳理上述逻辑:

  • 1、还是进行重复判断, 根据channelName从name2PubSubConnection中获取,看是否存在已经订阅:PubSubConnectionEntry; 如果存在直接把新的listener加入到PubSubConnectionEntry。
  • 2、从队列freePubSubConnections中取公用的PubSubConnectionEntry, 如果没有就进入connect()方法
    • 2.1 会根据subscriptionsPerConnection创建PubSubConnectionEntry, 然后调用其tryAcquire()方法 - 每调用一次就会减1
    • 2.2 将新的PubSubConnectionEntry放入全局的name2PubSubConnection, 方便后续重复使用;
    • 2.3 同时也将PubSubConnectionEntry放入队列freePubSubConnections中。- remainFreeAmount > 0
    • 2.4 后面就是进行底层的subscribeaddListener
  • 3、如果已经存在PubSubConnectionEntry,则利用已有的PubSubConnectionEntry进行tryAcquire;
  • 4、如果remainFreeAmount < 0 会抛出IllegalStateException异常;如果remainFreeAmount=0,则会将其从队列中移除, 那么后续请求会重新获取一个可用的连接
  • 5、最后也是进行底层的subscribeaddListener

三 总结

根因: 从上面代码分析, 导致问题的根因是因为PublishSubscribeService 会使用公共队列中的freePubSubConnections, 如果同一个key一次性请求超过subscriptionsPerConnection它的默认值5时,remainFreeAmount就可能出现-1的情况, 那么就会导致commandExecutor.syncSubscription(future)中等待超时,也就抛出如上异常Subscribe timeout: (7500ms). Increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters.

解决方法: 在初始化Redisson可以可指定这个配置项的值。

相关参数的解释以及默认值请参考官网:https://github.com/redisson/redisson/wiki/2.-Configuration#23-common-settings

Redisson-关于使用订阅数问题的更多相关文章

  1. 使用redisson时关于订阅数的问题

    在使用redisson消息订阅时,我针对门店商品库存减扣进行订阅的操作(在这里一个商品一个监听队列),当正式投入生产时,发现一直再报Subscribe timeout: (" + timeo ...

  2. Redisson 分布式锁实现之前置篇 → Redis 的发布/订阅 与 Lua

    开心一刻 我找了个女朋友,挺丑的那一种,她也知道自己丑,平常都不好意思和我一块出门 昨晚,我带她逛超市,听到有两个人在我们背后小声嘀咕:"看咱前面,想不到这么丑都有人要." 女朋友 ...

  3. Redis进阶篇:发布订阅模式原理与运用

    "65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?" "那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆 ...

  4. Redis 发布订阅

    订阅: class Program { //版本2:使用Redis的客户端管理器(对象池) public static IRedisClientsManager redisClientManager ...

  5. Redis教程03——Redis 发布/订阅(Pub/Sub)

    Pub/Sub 订阅,取消订阅和发布实现了发布/订阅消息范式(引自wikipedia),发送者(发布者)不是计划发送消息给特定的接收者(订阅者).而是发布的消息分到不同的频道,不需要知道什么样的订阅者 ...

  6. redis:消息发布与订阅频道

    1. 发布与订阅频道 消息发布与订阅像收音机与广播台的关系 1.1. publish channel message 发布频道 语法:publish channel message 作用:发布频道消息 ...

  7. Azure 订阅和服务限制、配额和约束

    最后更新时间:2016年10月24日 概述 本文档指定一些最常见的 Azure 限制.请注意,本文档目前未涵盖所有 Azure 服务.一段时间后,将展开并更新这些限制以包含多个平台. NOTE: 如果 ...

  8. Redis的订阅发布

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using ServiceS ...

  9. 微信小程序新服务消息推送 —— 订阅消息

    微信团队前不久公测了「订阅消息」,原有的小程序模板消息接口将于 2020 年 1 月 10 日下线,届时将无法发送模板消息.「订阅消息」将完全替代「模板消息」,这两天得空测试了一波. 1.下发权限机制 ...

随机推荐

  1. MySQL慢日志优化

    慢日志的性能问题 造成 I/O 和 CPU 资源消耗:慢日志通常会扫描大量非目的的数据,自然就会造成 I/O 和 CPU 的资源消耗,影响到其他业务的正常使用,有可能因为单个慢 SQL 就能拖慢整个数 ...

  2. Amazing!!CSS 也能实现极光?

    在上次写完这篇文章 -- 巧用渐变实现高级感拉满的背景光动画 之后,文章下面的评论有同学留言,使用 CSS 可以实现极光吗? 像是这样: emmm,这有点难为人了.不过,最近我也尝试着去试了下,虽然不 ...

  3. python开发环境软件包安装相关 failed with error code 1 in /tmp/pip-build-vn_f_e1n/psutil/

    指定源安装 pip install git+https://github.com/xxxxxx.git pip install -r requirements.txt -i https://mirro ...

  4. Nginx 编译数格式化输出

    printf "%s\n" `nginx -V 2>&1` nginx -V 2>&1 | sed 's/ /\n/g'

  5. .Net Core 项目发布在IIS上 访问404 问题对应

    对策: 1.进入线程池画面,将当前程序的线程池设为"无托管代码"   2.修改配置文件 Web.config,加上配置   原因: 因为.NetCore 5.0 自带集成了Swag ...

  6. UDP&串口调试助手用法(2)

    通道的是创建.删除.编辑.链接.断开 通道创建 通道删除 先选择要删除的通道,再点击删除通道即可 通道参数编辑 双击创建的通道 即可编辑通道 通道链接 通道创建成功,提示 点击链接即可链接通道 通道断 ...

  7. cmake之譬判断cmake的版本

    note 有时候,可能使用的cmake语法 与cmake的版本有关系, 比如modern cmake. 这时候我们可以在 CMAKELISTS.TXT中 判断 cmakeLists.txt 代码 if ...

  8. Android Linux vmstat 命令详解

    vmstat命令是最常见的Linux/Unix监控工具,可以展现给定时间间隔的服务器的状态值,包括服务器的CPU使用率,内存使用,虚拟内存交换情况,IO读写情况.这个命令是我查看Linux/Unix最 ...

  9. 【LeetCode】632. Smallest Range 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/smallest ...

  10. Nginx应用场景配置

    Nginx应用全入门 基础回顾 Nginx是什么? Nginx是一个高性能的HTTP和反向代理web服务器,特点是内存少,并发能力强 Nginx能做什么 Http服务器(Web服务器) 反向代理服务器 ...