注册

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

前言


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


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


image.png


解决思路


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


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);
}

image.png


Redis分布式锁


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


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



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

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


Mysql行锁


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


总结


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

作者:BLACK595
来源:juejin.cn/post/7398038222985543692

0 个评论

要回复文章请先登录注册