V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
abcbuzhiming
V2EX  ›  Java

要针对特定的条件进行加锁时,用什么方式是最佳实践?

  •  
  •   abcbuzhiming · 2020-06-25 16:44:07 +08:00 · 3250 次点击
    这是一个创建于 1613 天前的主题,其中的信息可能已经有所发展或是发生改变。
    大部分数据冲突,往往是用户之间数据冲突,比如秒杀业务,此时对共享资源上一般的锁,或者强迫用户排队,就可以得到解决。

    但是有一类情况是这样的,用户和用户之间并没有数据冲突,但是要防止用户“自己抢自己”,比如每日签到奖励这样的业务,1 个用户只有 1 次机会每天,用户和用户之间并没有争抢,我希望根据用户 id 来区分,如果多个线程进入到签到业务(可能是误按,也可能是恶意)时发现用户 id 是同一个,则只会有一个线程进入签到业务处理代码块,其它线程阻塞;但是同时,访问到这块代码时,不是这个用户 id 的线程不受影响,能继续执行
    22 条回复    2020-06-26 09:46:18 +08:00
    sujin190
        1
    sujin190  
       2020-06-25 16:55:47 +08:00
    对用户 ID 进行加锁就是了啊,如果复杂业务的还可以以一系列信息生成 hash id 来加锁,除了多线程常规的单进程锁外,也可以用 redis 、zookeeper 之类的外部服务来加锁也很方便的

    https://github.com/snower/slock

    用 go 实现过一个能用于此种场景的小服务,能实现锁的语义还挺多的,性能也不错
    reus
        2
    reus  
       2020-06-25 16:59:26 +08:00
    有区别吗?
    秒杀是锁商品,签到是锁用户,都是一样的逻辑,可以用一样的方法
    abcbuzhiming
        3
    abcbuzhiming  
    OP
       2020-06-25 17:47:39 +08:00
    @sujin190 我不太清楚 Go 的做法,但是 java 好像没找到这种方式
    abcbuzhiming
        4
    abcbuzhiming  
    OP
       2020-06-25 17:51:03 +08:00
    @reus 有区别的朋友,这个问题我也是思考过后才提的,秒杀场景里,所有用户争抢一把锁,用户和用户之间有数据冲突。签到场景里,你要防止的是用户抢自己的数据,用户和用户之间是不存在数据冲突的。

    所以你说签到锁用户,在 Java 里,这到底是怎么个锁法?才能即避免用户自己和自己的数据冲突,但是锁不影响到其它用户
    phx13ye
        5
    phx13ye  
       2020-06-25 18:36:29 +08:00 via Android
    你对用户 id 和日期做幂等啊
    EminemW
        6
    EminemW  
       2020-06-25 19:11:19 +08:00
    这跟语言没关系吧。
    用 date_userId 作为 key,存到 redis 里当锁,不就能解决了。
    skypyb
        7
    skypyb  
       2020-06-25 19:18:26 +08:00
    用中间件(zk 、redis) 就行了。业务线名字+用户 ID 拼出锁的 key 去获取就 OK
    yukiloh
        8
    yukiloh  
       2020-06-25 19:26:32 +08:00 via Android
    原来我遇到的点的快了给 2 次奖励是这么产生的…
    我觉得让前段做个判断,点了就 didable 不就好了,锁后台多麻烦
    chenqh
        9
    chenqh  
       2020-06-25 20:08:06 +08:00
    感觉直接 redis 锁就可以了吧,不管是锁所有人的,还是锁某个人的
    abcbuzhiming
        10
    abcbuzhiming  
    OP
       2020-06-25 21:07:58 +08:00
    @phx13ye 朋友,幂等指的是结果,而不是实现这个结果的过程,我问的是“要如何实现这个幂等”?
    abcbuzhiming
        11
    abcbuzhiming  
    OP
       2020-06-25 21:09:00 +08:00
    @EminemW
    @skypyb
    如果要借助第三方,不说 redis,zk,用 mysql 的多列唯一索引也能做。但是,我现在就是要在 JVM 特性层面上找解决方案
    phx13ye
        12
    phx13ye  
       2020-06-25 21:12:46 +08:00 via Android
    @abcbuzhiming 你不借助第三方,你的应用是单机跑的吗?
    abcbuzhiming
        13
    abcbuzhiming  
    OP
       2020-06-25 21:13:19 +08:00
    @yukiloh 前端加重复提交令牌确实可以把用户重复请求挡在前端(正常操作情况下),但是并不代表说后端就不需要防御了,实际上当后端规模拉大一点,某些内部事件激发的处理代码确实可能因为种种原因,被同时调用了多次,此时幂等是必要的。问题在于如何实现这个幂等,我比较贪心,想在 JVM 层面上解决这个问题,同时隔离影响,不影响到其它用户
    abcbuzhiming
        14
    abcbuzhiming  
    OP
       2020-06-25 21:14:58 +08:00
    @phx13ye 做研究嘛,有时候要看看一个语言到底能做到什么程度。否则 mysql 满地走的年代,为啥还有人用 java 写了个个叫 H2 的内存数据库呢?
    phx13ye
        15
    phx13ye  
       2020-06-25 21:25:42 +08:00 via Android
    @abcbuzhiming 。。。。看下 Semaphore 和 guava 的 Ratelimit 吧
    siweipancc
        16
    siweipancc  
       2020-06-25 21:28:28 +08:00 via iPhone
    :D 不用 redis? 那共享一个 map 实例行不行,setIfAbsent
    cheng6563
        17
    cheng6563  
       2020-06-25 21:31:49 +08:00 via Android
    单进程应用可以用个 Map 存 Lock 对象加锁
    Ezez
        18
    Ezez  
       2020-06-25 23:58:27 +08:00 via iPhone
    是不是可以数据库里加个字段表示当天是否签到?
    nuk
        19
    nuk  
       2020-06-26 00:32:20 +08:00
    这个可以不用锁啊,既然只执行一次,弄个 counter,atomic fetchadd 一下,等于 1 就执行,不等于就退出。。
    gaius
        20
    gaius  
       2020-06-26 00:37:44 +08:00 via Android
    🐶一个用户一个锁呗
    Licsber
        21
    Licsber  
       2020-06-26 00:59:33 +08:00
    为啥不签到只更新今日签到标记
    然后用一个单线程在那定时获取已经签到且未发送奖励的
    发送完奖励后在这个线程中更新已经发送标记
    aguesuka
        22
    aguesuka  
       2020-06-26 09:46:18 +08:00
    单应用可以用这个

    https://stackoverflow.com/questions/5639870/simple-java-name-based-locks

    All those answers I see are way too complicated. Why not simply use:

    public void executeInNamedLock(String lockName, Runnable runnable) {
    synchronized(lockName.intern()) {
    runnable.run();
    }
    }
    The key point is the method intern: it ensures that the String returned is a global unique object, and so it can be used as a vm-instance-wide mutex. All interned Strings are held in a global pool, so that's your static cache you were talking about in your original question. Don't worry about memleaks; those strings will be gc'ed if no other thread references it. Note however, that up to and including Java6 this pool is kept in PermGen space instead of the heap, so you might have to increase it.

    There's a problem though if some other code in your vm locks on the same string for completely different reasons, but a) this is very unlikely, and b) you can get around it by introducing namespaces, e.g. executeInNamedLock(this.getClass().getName() + "_" + myLockName);
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5209 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 05:42 · PVG 13:42 · LAX 21:42 · JFK 00:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.