多用户抢红包,如何保证只有一个抢到

前言

在一个百人群中,群主发了个红包,设置的3个人瓜分。如何能够保证只有3个人能抢到。100个人去抢,相当于就是100个线程去争夺这3个资源,如果处理不好,可能就会发生“超卖”,产生脏数据,威胁系统的正常运行。

当100个人同时去抢,也就是线程1,线程2,线程3…,此时线程1和线程2已经抢到了,就还剩一个红包了,而此时线程3和线程4同时发出抢红包的命令,线程3查询数据库发现还剩1个,抢下成功,而线程3还未修改库存时,线程4也来读取,发现还剩一个,也抢成功。结果这就发生“超卖”,红包被抢了4个,数据库一看红包剩余为-1。

在这里插入图片描述

解决思路

为了保证资源的安全,不能让多个用户同时访问到资源,也就是需要互斥的访问共有资源,同一时刻只能让一个用户访问,也就是给共享资源加上一个悲观锁,只有拿到锁的线程才能正常访问资源,拿不到锁的线程也不能让他一直等着,直接返回用户让他稍后重试。

JVM本地锁

JVM本地锁由ReentrantLock或synchronized实现

//抢红包方法加锁
public synchronized void grabRedPaper(){
    ...业务处理
}

不过这种同步锁粒度太大,我们需要的是针对抢同一红包的用户互斥,而这种方式是所有调用grabRedPaper方法的线程都需要等待,即限制所有人抢红包操作,效率低且不符合业务需求。每个红包应该都有一个唯一性ID,在单个红包上加锁效率就会高很多,也是单进程常用的使用方式。

private Map<String, Object> lockMap = new HashMap<>();

//抢红包方法
public void grabRedPaper(String redPaperId) {
    Object lock = getLock(redPaperId);
    synchronized (lock) {
        // 在这里进行对业务的互斥访问操作
    }
}
//获取红包ID锁对象
private Object getLock(String redPaperId) {
    if (!lockMap.containsKey(redPaperId)) {
        lockMap.put(redPaperId, new Object());
    }
    return lockMap.get(redPaperId);
}

在这里插入图片描述

Redis分布式锁

但当我们使用分布式系统中,一个业务功能会打包部署到多台服务器上,也就是会有多个进程来尝试获取共享资源,本地JVM锁也就无法完成需求了,所以我们需要第三方统一控制资源的分配,也就是分布式锁。

在这里插入图片描述

分布式锁一般一般需要满足四个基本条件:

  1. 互斥:同一时刻,只能有一个线程获取到资源。
  2. 可重入:获取到锁资源后,后续还能继续获取到锁。
  3. 高可用:锁服务一个宕机后还能有另一个接着服务;再者即使发生了错误,一定时间内也能自动释放锁,避免死锁发生。
  4. 非阻塞:如果获取不到锁,不能无限等待。

有关分布式锁的具体实现我之前的文章有讲到Java实现Redis分布式锁 - 掘金 (juejin.cn)

Mysql行锁

再者我们还可以通过Mysql的行锁实现,SELECT…FOR UPDATE,这种方式会将查询时的行锁住,不允许其他事务修改,直到读取完毕。将行锁和修改红包剩余数量放在一个事务中,也能做到互斥。不过这种做法效率较差,不推荐使用。

总结

方案实现举例优点缺点
JVM本地锁synchronized实现简单,性能较好只能在单个 JVM 进程内使用,无法用于分布式环境
Mysql行锁SELECT…FOR UPDATE保证并发情况下的隔离性,避免出现脏数据增加了数据库的开销,特别是在高并发场景下;对应用程序有一定的侵入性,需要在 SQL 语句中正确使用锁定机制。
分布式锁Redis分布式锁可用于分布式,性能较高实现相对复杂,需要考虑锁的续租、释放等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值