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
相关产品推荐
相关产品推荐

