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

如何将SMPP会话Java对象存储至Oracle数据库以实现Spring Boot多Pod间复用

如何将SMPP会话Java对象存储至Oracle数据库以实现Spring Boot多Pod间复用

我太懂你现在的处境了——22个Spring Boot Pod嗷嗷待哺,但SMPP服务器只给开5个会话配额,每个会话又能扛1000+请求,不用共享资源简直是暴殄天物。用Oracle这个共享DB来做会话的“调度中心”确实是个靠谱的思路,但先给你提个醒:SMPP会话对象本身是没法直接存数据库的,因为它绑定了当前Pod的网络连接、IO流这些活资源,序列化存成BLOB再读出来也没法用。咱们得换个思路:存会话的元数据和状态,用数据库做锁和资源调度,实现跨Pod的会话复用。

下面给你一步步拆解实现方案:

1. 先给Oracle建个会话管理表

首先得在Oracle里建一张表,用来记录每个SMPP会话的核心信息和状态,相当于咱们的“会话注册表”。这里给你个参考的建表SQL:

CREATE TABLE SMPP_SESSIONS (
    ID NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    SMPP_SERVER VARCHAR2(100) NOT NULL, -- SMPP服务器地址
    SMPP_PORT NUMBER NOT NULL, -- SMPP端口
    SYSTEM_ID VARCHAR2(50) NOT NULL, -- 账号ID
    PASSWORD VARCHAR2(50) NOT NULL, -- 密码
    STATUS VARCHAR2(20) DEFAULT 'DISCONNECTED' CHECK (STATUS IN ('CONNECTED', 'IDLE', 'IN_USE', 'DISCONNECTED')), -- 会话状态
    LAST_ACTIVE_TIME TIMESTAMP DEFAULT SYSTIMESTAMP, -- 最后活跃时间,用来判断会话是否存活
    OWNER_POD_ID VARCHAR2(100), -- 当前持有会话的Pod标识(可选,方便排查问题)
    CREATED_TIME TIMESTAMP DEFAULT SYSTIMESTAMP
);

-- 给状态字段建索引,提升查询效率
CREATE INDEX IDX_SMPP_SESSIONS_STATUS ON SMPP_SESSIONS(STATUS);

2. 核心逻辑:用DB做会话的锁和调度器

咱们的目标是让22个Pod共享5个会话,核心就是同一时间只有一个Pod能使用某个会话,数据库的行级锁刚好能帮咱们实现这个“抢资源”的逻辑。具体流程是这样的:

  • 当某个Pod需要发送短信时,先去DB里找状态为CONNECTED或IDLE的会话,用SELECT ... FOR UPDATE SKIP LOCKED语法获取一个未被锁定的会话(这个语法是Oracle 12c+支持的,能直接跳过已经被其他Pod锁定的行,避免大家排队等同一个会话)。
  • 拿到会话后,把它的状态改成IN_USE,标记当前Pod为持有者,然后用会话的元数据(服务器地址、账号密码这些)初始化或复用连接。
  • 用完会话后,把状态改回IDLE,更新最后活跃时间,释放锁让其他Pod能用。
  • 如果没有可用会话,就检查当前已创建的会话数量是否达到5个上限,没到的话就新建一个会话,并存入DB;已经到上限的话,就等待一会儿再重试,或者抛出异常提示资源不足。

3. 会话生命周期的维护

光有调度还不够,得确保会话的状态是准确的,避免出现“僵尸会话”:

  • 心跳机制:每个持有会话的Pod,每隔10-30秒就更新一次该会话的LAST_ACTIVE_TIME,证明自己还在正常使用。
  • 僵尸会话清理:写一个定时任务(比如用Spring的@Scheduled),定期扫描会话表,把LAST_ACTIVE_TIME超过阈值(比如1分钟)且状态为IN_USE的会话,强制改成DISCONNECTED,释放资源。
  • 断开重连:当Pod检测到SMPP会话断开时,立即更新DB里的会话状态为DISCONNECTED,然后释放锁,让其他Pod可以重新初始化这个会话。

4. Spring Boot里的代码实现示例

给你写个简化版的会话管理器Bean,大概是这个样子:

@Component
public class SmppSessionManager {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // SMPP服务器允许的最大会话数
    private static final int MAX_SESSIONS = 5;
    // 会话超时阈值(比如1分钟)
    private static final long SESSION_TIMEOUT_SECONDS = 60;

    public SmppSession getAvailableSession() throws RuntimeException {
        // 先尝试获取可用的会话元数据
        SmppSessionMetadata metadata = fetchAvailableSessionMetadata();
        if (metadata != null) {
            // 检查会话是否还存活
            if (isSessionAlive(metadata)) {
                // 标记会话为正在使用
                markSessionAsInUse(metadata.getId());
                // 根据元数据获取或创建会话连接(这里假设你有封装好的SMPP连接工具类)
                return SmppConnectionUtil.getOrCreateSession(metadata);
            } else {
                // 会话已失效,更新状态为断开,然后尝试新建
                updateSessionStatus(metadata.getId(), "DISCONNECTED");
                return createNewSession();
            }
        } else {
            // 没有可用会话,检查是否能新建
            int currentSessionCount = getCurrentSessionCount();
            if (currentSessionCount < MAX_SESSIONS) {
                return createNewSession();
            } else {
                throw new RuntimeException("所有SMPP会话都在使用中,请稍后重试");
            }
        }
    }

    // 获取可用的会话元数据(带行级锁)
    private SmppSessionMetadata fetchAvailableSessionMetadata() {
        String sql = "SELECT id, smpp_server, smpp_port, system_id, password, last_active_time " +
                     "FROM smpp_sessions " +
                     "WHERE status IN ('CONNECTED', 'IDLE') " +
                     "FOR UPDATE SKIP LOCKED " +
                     "FETCH FIRST 1 ROW ONLY";
        try {
            return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
                SmppSessionMetadata metadata = new SmppSessionMetadata();
                metadata.setId(rs.getLong("id"));
                metadata.setSmppServer(rs.getString("smpp_server"));
                metadata.setSmppPort(rs.getInt("smpp_port"));
                metadata.setSystemId(rs.getString("system_id"));
                metadata.setPassword(rs.getString("password"));
                metadata.setLastActiveTime(rs.getTimestamp("last_active_time"));
                return metadata;
            });
        } catch (EmptyResultDataAccessException e) {
            return null; // 没有可用会话
        }
    }

    // 检查会话是否存活
    private boolean isSessionAlive(SmppSessionMetadata metadata) {
        long elapsedSeconds = ChronoUnit.SECONDS.between(metadata.getLastActiveTime().toInstant(), Instant.now());
        return elapsedSeconds < SESSION_TIMEOUT_SECONDS;
    }

    // 其他辅助方法:markSessionAsInUse、updateSessionStatus、createNewSession、getCurrentSessionCount等
    // 这里就不一一写全了,核心就是操作数据库更新状态和统计数量
}

关键注意事项

  • 千万别存会话对象本身:再强调一遍,SMPP会话里的socket连接、IO流都是和当前进程绑定的,序列化存进DB完全没用,必须存元数据,按需重建连接。
  • 行级锁的使用:FOR UPDATE SKIP LOCKED是这个方案的核心,能避免多个Pod同时争抢同一个会话,提升并发效率。
  • 定时任务要靠谱:清理僵尸会话的定时任务必须保证稳定运行,不然会出现会话资源被占着用不了的情况。

这样一套下来,就能让22个Spring Boot Pod乖乖共享那5个SMPP会话了,既符合SMPP服务器的限制,又能最大化利用资源。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 08:39:36