kafka高吞吐持久化方案(2)

进阶篇

文章主要针对上篇中方案3的进阶进行升级对比介绍

WeakKeyLockManager版本

使用 WeakHashMap + ReentrantLock 实现按 Key(如 uid_cid)自动回收的锁池(Lock Pool),适用于需要对不同 key 粒度加锁,又希望避免内存泄漏的场景。

✅ 1. WeakKeyLockManager(支持按 key 自动回收的锁)

java 复制代码
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantLock;

public class WeakKeyLockManager<K> {

    // 用于保护 WeakHashMap 本身的线程安全
    private final Object mapLock = new Object();

    // 使用 WeakHashMap 自动回收 key 不再引用的锁
    private final WeakHashMap<K, ReentrantLock> lockMap = new WeakHashMap<>();

    public ReentrantLock getLock(K key) {
        // 为什么要全局加锁?防止一个key对应多个锁。以及容易踩坑
        synchronized (mapLock) {
            return lockMap.computeIfAbsent(key, k -> new ReentrantLock());
        }
    }
}

synchronized(key)容易踩坑案例

java 复制代码
String key1 = new String("123_456"); // new 出来的对象
String key2 = new String("123_456");

System.out.println(key1 == key2); // false!!

这两个 synchronized完全独立的两个锁,不会互斥!

所以 synchronized(key) 只在你100% 保证 key 是相同对象引用时 才有效(注意,不是 equals 相等,而是 引用相等:==)。

更隐蔽的情况:即使你用的是 "123_456" 字面量

java 复制代码
String k1 = "123_456"; // 字面量常量池
String k2 = "123" + "_" + "456"; // 编译期优化后也一样
System.out.println(k1 == k2); // true ✅

synchronized (k1) {
    // 线程A
}
synchronized (k2) {
    // 线程B,能互斥 ✅
}

这在编译期是同一个对象引用,互斥有效。


❗ 但如果是运行时拼接,就失效了:

java 复制代码
String k1 = "123_456";
String k2 = String.valueOf(123) + "_" + String.valueOf(456);
System.out.println(k1 == k2); // false ❌

k1 != k2synchronized(k1)synchronized(k2)不会互斥

使用intern

缺点:

  • intern() 会把字符串放进 JVM 字符串常量池;
  • key 数量多时容易造成 常量池内存泄漏/膨胀
  • 对高并发系统来说风险较大。

✅ 为什么这个结构安全?

  • WeakHashMap 会在 key 没有外部强引用时自动移除 entry;
  • 保证了当 key(如 uid_cid 组合)不再使用后,对应的锁也能自动被 GC;
  • 使用 synchronized 是为了保护 lockMap 多线程访问的安全(你也可以改用 Collections.synchronizedMap(...) 或显式 ReadWriteLock);

✅ 2. 示例使用代码

java 复制代码
public class UpsertService {

    private final WeakKeyLockManager<String> lockManager = new WeakKeyLockManager<>();

    public void upsert(Long uid, Long cid, YourDataDTO data) {
        String key = uid + "_" + cid;
        ReentrantLock lock = lockManager.getLock(key);

        lock.lock();
        try {
            // 此处是数据库 upsert 的操作
            System.out.println("Thread " + Thread.currentThread().getName() + " processing: " + key);
            Thread.sleep(50); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

✅ 3. 测试类(模拟并发)

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class LockTest {
    public static void main(String[] args) {
        UpsertService service = new UpsertService();
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        // 模拟多线程处理 3 个不同 key,并发冲突
        for (int i = 0; i < 100; i++) {
            final long uid = i % 3;
            final long cid = i % 2;
            threadPool.submit(() -> service.upsert(uid, cid, null));
        }

        threadPool.shutdown();
    }
}

输出结果(简略):

plain 复制代码
Thread pool-1-thread-1 processing: 1_1
Thread pool-1-thread-3 processing: 0_0
Thread pool-1-thread-2 processing: 2_1
Thread pool-1-thread-4 processing: 1_1   <-- 同一个 key 会排队执行

🧠 补充说明

项目 说明
锁是否自动释放? 是的,WeakHashMap 中的 key(uid_cid )如果没有外部强引用,就会被 GC
是否线程安全? 是,getLock() 用了 synchronized ;如追求更高性能,可用 ConcurrentMap<K, WeakReference<ReentrantLock>>
是否锁冲突少? 是,每个 uid+cid 都能拥有自己的锁,精度高,冲突少

🚀 进阶(更高性能)

如果你业务量非常大,可以改为:

java 复制代码
Map<K, WeakReference<ReentrantLock>> map = new ConcurrentHashMap<>();
  • 每次 get() 时判断 ref.get() 是否已被 GC。
  • 避免同步块,提高并发性能。

进阶实现方案2

java 复制代码
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;

public class KeyLockManager<K> {

    private static class LockWrapper<K> extends WeakReference<Object> {
        final K key;
        LockWrapper(K key, Object referent, ReferenceQueue<Object> queue) {
            super(referent, queue);
            this.key = key;
        }
    }

    private final ConcurrentHashMap<K, LockWrapper<K>> lockMap = new ConcurrentHashMap<>();
    private final ReferenceQueue<Object> refQueue = new ReferenceQueue<>();

    public Object getLock(K key) {
        cleanStaleEntries();
        while (true) {
            LockWrapper<K> wrapper = lockMap.get(key);
            Object lock = wrapper != null ? wrapper.get() : null;

            if (lock != null) {
                return lock;
            }

            Object newLock = new Object();
            LockWrapper<K> newWrapper = new LockWrapper<>(key, newLock, refQueue);
            LockWrapper<K> existing = lockMap.putIfAbsent(key, newWrapper);
            if (existing == null) {
                return newLock;
            }
            Object existingLock = existing.get();
            if (existingLock != null) {
                return existingLock;
            }
            lockMap.replace(key, existing, newWrapper);
        }
    }

    private void cleanStaleEntries() {
        LockWrapper<K> ref;
        while ((ref = (LockWrapper<K>) refQueue.poll()) != null) {
            lockMap.remove(ref.key, ref);
        }
    }
}

使用方式

java 复制代码
KeyLockManager<String> lockManager = new KeyLockManager<>();

public void upsert(String uid, String cid) {
    String key = uid + "_" + cid;
    Object lock = lockManager.getLock(key);

    synchronized (lock) {
        // 安全执行你的 upsert 操作
        System.out.println(Thread.currentThread().getName() + " -> " + key);
    }
}

将原有的mapLock替换为新的锁对象池获取:lockManager.getLock(key)。支持key粒度的锁提升并发。但是实现更复杂成本也相对下一个方案更高

✅ 总结

  • WeakKeyLockManager<K> 利用了 Java 的 GC 特性,对 key 精准加锁且无内存泄漏风险
  • 在高并发场景中,它是介于 "分段锁" 和 "全量锁池" 之间的优雅方案;
  • 同时具备:高并发 + 自动清理 + 精确锁粒度 的优势。

线程安全、无锁实现

✅ 线程安全、无锁实现:基于 ConcurrentHashMap<WeakReference<ReentrantLock>> 的按 key 自动清理锁管理器

这是一个高性能、支持自动回收、避免锁泄露 的锁池方案 ------ 比 WeakHashMap 更适用于并发环境,推荐用于高并发下的"按 key 精准加锁"。


✅ 为什么要用 ConcurrentHashMap<WeakReference<Lock>>

  • ConcurrentHashMap 支持并发读写(比 WeakHashMap 更适合多线程);
  • WeakReference<ReentrantLock> 可以避免锁泄漏(key 无引用时,锁可以被回收);
  • 加上 ReferenceQueue,我们可以主动清理无效锁对象,避免锁池膨胀。

🚀 实现代码

🔧 WeakConcurrentLockManager<K>

java 复制代码
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

public class WeakConcurrentLockManager<K> {

    private static class LockReference<K> extends WeakReference<ReentrantLock> {
        final K key;

        LockReference(K key, ReentrantLock referent, ReferenceQueue<ReentrantLock> q) {
            super(referent, q);
            this.key = key;
        }
    }

    private final ConcurrentHashMap<K, LockReference<K>> lockMap = new ConcurrentHashMap<>();
    private final ReferenceQueue<ReentrantLock> queue = new ReferenceQueue<>();

    /**
     * 获取指定 key 对应的锁,若不存在则创建。
     */
    public ReentrantLock getLock(K key) {
        cleanStaleLocks();

        while (true) {
            LockReference<K> ref = lockMap.get(key);
            ReentrantLock existingLock = ref == null ? null : ref.get();
            if (existingLock != null) {
                return existingLock;
            }

            // 若当前无有效锁,则尝试创建新的锁
            ReentrantLock newLock = new ReentrantLock();
            LockReference<K> newRef = new LockReference<>(key, newLock, queue);

            // putIfAbsent,防止多个线程同时创建同一个锁
            LockReference<K> prev = lockMap.putIfAbsent(key, newRef);
            if (prev == null) {
                return newLock;
            }

            ReentrantLock prevLock = prev.get();
            if (prevLock != null) {
                return prevLock;
            } else {
                // 已有的锁被GC掉,尝试替换
                lockMap.replace(key, prev, newRef);
            }
        }
    }

    /**
     * 清理已被 GC 的锁引用
     */
    private void cleanStaleLocks() {
        LockReference<K> ref;
        while ((ref = (LockReference<K>) queue.poll()) != null) {
            lockMap.remove(ref.key, ref);
        }
    }
}

✅ 使用方式

java 复制代码
public class UpsertService {

    private final WeakConcurrentLockManager<String> lockManager = new WeakConcurrentLockManager<>();

    public void upsert(String uid, String cid) {
        String key = uid + "_" + cid;
        ReentrantLock lock = lockManager.getLock(key);
        lock.lock();
        try {
            // 这里处理 upsert 逻辑
            System.out.println(Thread.currentThread().getName() + " processing: " + key);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

✅ 测试代码(并发场景模拟)

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestApp {
    public static void main(String[] args) {
        UpsertService service = new UpsertService();
        ExecutorService executor = Executors.newFixedThreadPool(8);

        for (int i = 0; i < 100; i++) {
            final String uid = String.valueOf(i % 3);
            final String cid = String.valueOf(i % 2);
            executor.submit(() -> service.upsert(uid, cid));
        }

        executor.shutdown();
    }
}

✅ 输出示例(说明同一个 key 串行)

plain 复制代码
pool-1-thread-1 processing: 1_1
pool-1-thread-3 processing: 0_0
pool-1-thread-2 processing: 2_1
...

✅ 优点总结

特性 说明
🔒 精确按 key 加锁 每个 key 单独一把锁
🧠 自动回收锁 使用 WeakReference ,key 无引用时锁自动清理
⚙️ 高性能 基于 ConcurrentHashMap ,适合并发环境
✅ 线程安全 所有锁获取和清理均线程安全

🚫 注意事项

  • key 类型必须正确实现 equals()hashCode()
  • 如果你频繁创建短命 key,需要保证 key 外部也有引用(防止锁被 GC 太早);
  • 如果你不希望锁被 GC,可以使用 ConcurrentHashMap<K, Lock> 但要手动清理;

方案对比

非常好!我们现在来深入对比 你提到的两个加锁方案在"按 key 精准加锁"场景下的使用效果。


🎯 目标场景

并发环境下,业务根据 (uid, cid) 或封装后的 key 进行批量 upsert、避免写冲突,需要对每个 key 加锁,但不同 key 之间可并发。


🆚 对比双方

方案 简称
1️⃣ synchronized + Map<UniqueKey, Object> 有锁方案(全局锁 map)
2️⃣ ConcurrentHashMap<K, WeakReference<Lock>> + ReferenceQueue 无锁方案(细粒度并发锁池)

🧠 示例实现简述

✅ 有锁方案:synchronized + WeakHashMap

java 复制代码
Map<UniqueKey, Object> lockMap = new WeakHashMap<>();

public synchronized Object getLock(UniqueKey key) {
    return lockMap.computeIfAbsent(key, k -> new Object());
}
  • 通过全局 **synchronized** 确保 **WeakHashMap** 的线程安全
  • 自动回收未使用的 key 对应的锁对象
  • synchronized (getLock(key)) 实现互斥

✅ 无锁方案:ConcurrentHashMap<K, WeakReference<ReentrantLock>> + 清理

java 复制代码
// 封装类中
public ReentrantLock getLock(K key) {
    // 使用 putIfAbsent / replace 保证线程安全
    // 用 ReferenceQueue 清除已 GC 的锁
}
  • 每个 key 映射一个 ReentrantLock,多线程安全创建
  • 支持高并发、自动回收无效锁
  • 使用 lock.lock() / lock.unlock() 实现互斥

✅ 核心维度对比

对比维度 synchronized + Map (全局锁) ConcurrentMap + WeakReference (无锁方案)
🧵 线程安全性 依赖 synchronized 全局互斥,线程安全,但易阻塞 无需全局锁,使用原子操作 + 线程安全结构,线程安全 ✅
🚀 并发性能 ❌ 差:获取锁需要串行进入 getLock() ✅ 高:并发获取锁时几乎无冲突,适合多核、高并发
🧹 锁对象自动清理 ✅ 是(借助 WeakHashMap ✅ 是(WeakReference + ReferenceQueue
🔁 锁复用效率 中:容易重复创建 Object 锁(锁粒度不够强) 高:精确映射每个 key → ReentrantLock
🔒 锁功能 只能用 synchronized ,不可中断,不支持 tryLock 可选:tryLock、lockInterruptibly,灵活
🤯 错误使用风险 高:忘记同步、传入非唯一 key,容易失效 低:统一封装管理、线程安全构造
🧰 扩展能力 弱:不可做限时锁等高级功能 强:可支持监控、排队、超时等策略
🪵 日志调试能力 差:Object 锁难定位 好:每个 ReentrantLock 可打印调试信息

✅ 总结建议

适用场景 建议使用方案
🧪 简单 demo,单线程测试、小量数据 synchronized + Map (可快速验证)
⚙️ 高并发、批量 upsert、热点 key 多 ✅ 推荐:ConcurrentMap + WeakReference<Lock>
📦 希望封装通用锁池工具 ✅ 推荐封装类:如 KeyLockManager<K>
📊 需要做 tryLock / timeout / metrics ✅ 强烈建议 ReentrantLock 方案

✅ 性能对比结论(大量 key 并发时)

场景 全局锁方案(Map + synchronized) 无锁方案(ConcurrentMap + WeakReference)
🔁 10万个 key,20线程并发获取锁 ❌ 严重阻塞,吞吐低 ✅ 基本无锁冲突,吞吐优
🔁 同一个 key 并发抢锁 ✅ 都能互斥 ✅ 同样互斥(更灵活)
🔁 空闲 key 是否回收 ✅ 是 ✅ 是(更可控)

🧩 如果你只记一句话:

🔥「全局 synchronized 是阻塞瓶颈,只适合低并发;高并发时,应使用基于 ConcurrentHashMap + WeakReference 的无锁锁池来管理 key 级锁对象。」


Q&A

为什么不用synchronized(UniqueKey.hashCode)?

synchronized (UniqueKey.hashCode) 这种方式,看起来简单粗暴 ,但实际上有几个严重问题,它并不是线程安全、稳定、可维护的解决方案。


❌ 为什么不能用 synchronized(UniqueKey.hashCode())

1️⃣ hashCode() 只是一个 int 值,不是对象!
java 复制代码
int hash = key.hashCode();
synchronized (hash) { ... } // 编译报错!
  • synchronized 需要锁的是一个对象引用(Object),不是基本类型。
  • hashCode() 返回的是 int,你不能对 int 加锁。

你可能会写成:

java 复制代码
synchronized (Integer.valueOf(key.hashCode())) { ... }
2️⃣ 这其实是锁住了一个新对象!根本没起到锁的作用!
java 复制代码
synchronized (Integer.valueOf(key.hashCode())) {
    // 实际上每次都 new 了一个新的 Integer(除非值在 -128~127 缓存范围)
}
  • 即使你写 Integer.valueOf(...),也几乎总是生成新的 Integer 对象(除非在 [-128, 127] 范围内,会被 Integer 缓存)。
  • 这就意味着:每次锁的对象不同,完全无法互斥。

❌ 继续演进:尝试缓存锁对象

你可能会想:

那我是不是用一个 Map<Integer, Object> 来缓存锁对象呢?

java 复制代码
Map<Integer, Object> lockMap = new ConcurrentHashMap<>();

Object lock = lockMap.computeIfAbsent(key.hashCode(), k -> new Object());
synchronized (lock) {
    // do upsert
}

这也有严重问题

3️⃣ hashCode() 不唯一!不同 key 可能 hash 相同,发生误锁
  • hashCode() 是哈希值,不是唯一 ID。
  • 两个不同的 (uid, cid) 可能有相同的 hashCode,但你不希望它们共享锁。
  • 会导致 不同业务数据不必要地串行,性能下降,或者更严重的死锁风险(如果你用双锁)。

✅ 正确的替代方案:锁分段/对象池

我们不应该对 hashCode 本身加锁,而是使用:

✅ 分段锁机制(推荐)
java 复制代码
int slotCount = 256;
ReentrantLock[] lockPool = new ReentrantLock[slotCount];
// init locks

int slot = (key.hashCode() & Integer.MAX_VALUE) % slotCount;
ReentrantLock lock = lockPool[slot];

lock.lock();
try {
    // 线程安全处理
} finally {
    lock.unlock();
}

优点:

  • 控制锁数量,内存可控;
  • 不同 key hash 到不同槽 → 并发处理;
  • 同一 key 总 hash 到同一个槽 → 顺序处理;
  • 性能比 **synchronized** 更可控、粒度更细、死锁概率更低。

🔒 或者高级点:每个 UniqueKey 对应一个独立锁对象

这种方式可以用 LoadingCache<UniqueKey, Lock>ConcurrentHashMap<UniqueKey, ReentrantLock> 来实现,但要注意:

  • 要定期清理 WeakReference 防止内存泄漏;
  • 用于超高并发热点 key 更精准控制。

✅ 总结对比

方法 是否可行 问题 备注
synchronized(hashCode()) ❌ 不可行 加锁对象不是同一个 锁失效
synchronized(Integer.valueOf(hash)) ❌ 不可行 Integer 缓存范围有限 锁失效
Map<Integer, Object> + synchronized ⚠️ 勉强可行 hash 冲突严重 精度差,不推荐
✅ 分段锁 + ReentrantLock[] ✅ 可行 轻量、性能好 推荐 ✅
ConcurrentHashMap<UniqueKey, Lock> ✅ 精细控制 内存大、需回收 热点业务适用

synchronized(String.valueOf(uid_cid)) ------ 这个方式在代码层面是可编译、可运行的,但它 看似能锁住相同的 uid+cid,却实际存在严重缺陷。我们来逐一剖析。


sychronized(String.valueOf(uid_cid))呢?

🧨 1. String.valueOf(uid_cid) 每次都是新对象!

假设你写的是:

java 复制代码
String key = String.valueOf(uid + "_" + cid);
synchronized (key) {
    // do upsert
}

这个 key 是一个新的 String 实例,除非你手动 intern,否则:

每次加锁的对象都不一样,根本没有加锁效果!

java 复制代码
System.out.println(new String("1_2") == new String("1_2")); // false
  • synchronized 是基于对象引用加锁的。
  • 如果每次创建一个新字符串,就等于每次 synchronized(new Object()),完全失效。

✅ 2. 使用 .intern() 看似可行,但问题更大!

有些人会尝试这样:

java 复制代码
String key = (uid + "_" + cid).intern();
synchronized (key) {
    // ...
}

这会将 key 放入 JVM 的字符串常量池,从而达到相同 uid_cid 用同一个对象加锁的目的。

但注意⚠️:

❌ 危险点:内存泄漏风险极高!!!
  • String.intern() 会把字符串永久保存到 JVM 的常量池中(在 Java 8 之后移到了堆中,但仍不可回收,除非整个 key 无引用)。
  • 如果你有大量 uid+cid 组合(成千上万),就会不断往字符串常量池里加对象,永久无法回收!
  • 严重时可能造成 **OutOfMemoryError: GC overhead limit exceeded****Metaspace** 溢出。

🧠 所以总结:

方法 可行性 缺点
synchronized(String.valueOf(...)) 每次新对象,无法互斥
synchronized((uid + "_" + cid).intern()) ⚠️ 有效但高风险 内存泄漏
✅ 分段锁(ReentrantLock[]) ✅ 推荐 性能好、无泄漏
ConcurrentHashMap<UniqueKey, Lock> + WeakReference ✅ 高级 控制好内存,可精准锁

🔚 总结一句话:

**synchronized(String.valueOf(...))**** 是"看起来能锁,实际不会锁"的经典陷阱。用 ReentrantLock + 分段锁才是靠谱的并发控制方式。**


相关推荐
永亮同学3 小时前
【探索实战】告别繁琐,一栈统一:Kurator 从0到1落地分布式云原生应用管理平台!
分布式·云原生
十五年专注C++开发4 小时前
ZeroMQ: 一款高性能、异步、轻量级的消息传输库
网络·c++·分布式·zeroqm
张人玉5 小时前
LiveCharts WPF MVVM 图表开发笔记
大数据·分布式·wpf·livecharts
不惑_5 小时前
Kurator 分布式云原生平台从入门到实战教程
分布式·云原生
一起养小猫5 小时前
【贡献经历】从零到贡献者:我的Kurator开源社区参与之旅
分布式·物联网·云原生·开源·华为云·istio·kurator
2501_940198696 小时前
【前瞻创想】Kurator云原生实战:从入门到精通,打造分布式云原生新生态
分布式·云原生
Wang's Blog6 小时前
RabbitMQ: 消息发送失败的重试机制设计与实现
分布式·rabbitmq
free-elcmacom7 小时前
机器学习高阶教程<8>分布式训练三大核心策略拆解
人工智能·分布式·python·机器学习