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

Telegram bot用户保存按ConcurrentHashMap key加synchronized锁是否正确

问题解答

改造方案正确性判断

你的改造思路是对的,即通过细粒度的用户级锁替代全局锁实现不同用户请求并行处理,但当前实现存在1个严重bug:

  • ConcurrentHashMap.putIfAbsent() 的返回值规则是:如果key不存在,成功插入新值后返回null;如果key已存在,返回之前存储的旧值。你直接对返回值用!!强制非空,第一次处理新用户时会直接抛出空指针异常。

修复方案非常简单,调整userId的取值逻辑即可:

val userId = users.putIfAbsent(tUser.id, tUser.id) ?: tUser.id

修复后锁逻辑是正确的:因为同一个用户ID对应的value始终是同一个Long实例,synchronized(userId)能保证同一用户的读写操作串行执行,不同用户的操作完全并行。


ConcurrentHashMap阻塞优化方案

你观察到的ConcurrentHashMap处的阻塞是正常现象:Java 8+的ConcurrentHashMap采用CAS+Node级别的synchronized实现并发控制,大量线程同时操作同一个哈希槽时会出现非常轻微的锁阻塞,这个开销远低于数据库操作的开销,如果需要进一步优化可以参考以下方案:

方案1:改用分片锁(Striped Lock),消除全量锁对象维护开销

不需要为每个用户单独维护锁对象,可以预设固定数量的锁分片,相同用户ID哈希到同一个分片锁即可,内存占用固定,没有putIfAbsent的写入开销:

// 示例:预设64个锁分片,可根据并发量调整大小
private val locks = Array(64) { Any() }

fun save(tUser: org.telegram.telegrambots.meta.api.objects.User): User {
    val userId = tUser.id
    // 取模计算分片索引,注意要处理负数哈希值
    val lockIdx = (userId.hashCode() and Int.MAX_VALUE) % locks.size
    synchronized(locks[lockIdx]) {
        // 原有业务逻辑不变
        val user = userRepository.findById(userId).orElse(User(userId))
        user.apply {
            username = tUser.userName
            firstName = tUser.firstName
            lastName = tUser.lastName
            languageCode = tUser.languageCode
            lastMessageReceived = Instant.now()
        }
        return userRepository.save(user)
    }
}

该方案的缺点是极端情况下会出现不同用户的锁冲突,但是冲突概率极低,对性能的影响可以忽略。

方案2:用数据库原子操作替代JVM锁(最推荐)

如果你的底层数据库支持原子upsert操作,完全可以去掉JVM层面的所有锁,直接用数据库的原子性保证操作串行:

  • MySQL可以用INSERT ... ON DUPLICATE KEY UPDATE
  • PostgreSQL可以用INSERT ... ON CONFLICT (user_id) DO UPDATE

单条语句即可完成用户不存在则插入、存在则更新的逻辑,不仅性能更高,还能解决多实例部署时JVM锁失效的问题。

方案3:优化锁缓存的内存回收

如果必须保留按用户ID加锁的逻辑,可以改用弱引用存储锁对象,定期清理不活跃的锁,避免ConcurrentHashMap内存持续上涨:

private val users = ConcurrentHashMap<Long, WeakReference<Long>>()

fun save(tUser: org.telegram.telegrambots.meta.api.objects.User): User {
    val userId = tUser.id
    val lock = users.compute(userId) { _, oldRef ->
        oldRef?.takeIf { it.get() != null } ?: WeakReference(userId)
    }!!.get()!!
    synchronized(lock) {
        // 原有业务逻辑不变
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 15:06:06