探讨Redis分布式锁解决优惠券拼抢问题

本文详细介绍了分布式锁的概念、必要性及其常见实现方式,包括基于MySQL、Redis和Zookeeper的解决方案。重点讨论了使用Redis实现分布式锁的步骤,通过实例分析了并发场景下可能遇到的问题及优化措施,如设置过期时间、加锁续命等。最后,文章提到了Redission框架如何优雅地解决分布式锁的问题,通过lua脚本确保加锁的原子性,并分析了其内部实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是分布式锁

分布式锁是控制不同系统之间访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来确保数据一致性。

二、为什么需要分布式锁

在单机部署的系统中,一般采用线程锁来解决高并发的问题,多线程访问共享变量的问题达到数据一致性,可以使用synchornizedReentrantLock等。但是对于分布式集群部署的系统架构,程序在不同的JVM虚拟机中运行,且因为synchronized或ReentrantLock都只能保证同一个JVM进程中保证有效,所以这时就需要使用分布式锁了。

三、分布式锁的实现方式

  • 基于MySQL数据库实现分布式锁
  • 基于Redis实现分布式锁(Redission)
  • 基于Zookeeper实现分布式锁
  • 自研分布式锁:比如谷歌的Chubby

目前主流的分布式锁的实现方式:基于数据库的分布式锁、基于Redis实现的分布式锁、基于zookeeper实现分布式锁 三种,本文主要介绍采用Redis的方式实现分布式锁。

四、分布式锁的应用实例

优惠券拼抢的场景为🌰说明,一般的话在818或者其他购物节到来之前,系统都会预先发放对应的优惠券,假设在某一天,系统放发乐一个满减优惠券couponTicket,发放数量为50:,通过伪代码的方式模拟一下多线程抢券的场景:

4.1 单机模式解决并发问题

/**
 * 营销优惠券
 *
 * @author: jacklin
 * @date: 2022/8/17 14:58
 */
@Slf4j
@RestController
public class MarketCouponController {

    @Resource
    private Redisson redisson;
    @Resource
    private StringRedisTemplate redisTemplate;

    @RequestMapping("/deductCouponTicket")
    public String deductCouponTicket(String couponCode) {
        String lockKey = "coupon:ticket" + couponCode;
        int couponTicketCount = Integer.parseInt(redisTemplate.opsForValue().get(lockKey));
        if (couponTicketCount > 0) {
            int realCouponTicketCount = couponTicketCount - 1;
            log.info("用户抢券成功,扣减优惠券数量,剩余量:" + realCouponTicketCount);
            redisTemplate.opsForValue().set(lockKey, realCouponTicketCount + "");
        } else {
            log.error("用户抢券失败,优惠券已被抢光!");
        }
        return "end";
    }
}
复制代码

🤔代码思考分析:不难发现,以上的代码很明显存在一个问题,如果在同一时间两个线程thread1、thread2同时请求进来,通过redisTemplate.opsForValue().get(lockKey)拿到优惠券剩余量都是50,当执行完成后thread1和thread2又将减完后的库存couponTicket=49重新写入Redis,那么数据就会产生问题,实际上两个线程都抢券成功,实现应该各减去了一张券码数,然而实际写进就只减了一次券码数,就出现了数据不一致的现象。

上述的问题产生的原因主要就是从Redis拿数去到扣减优惠券数量不是原子性操作,在以往的单体项目中,解决这种问题可以采用synchronized同步锁的方式去实现:

@Slf4j
@RestController
public class MarketCouponController {

    @Resource
    private Redisson redisson;
    @Resource
    private StringRedisTemplate redisTemplate;

    @RequestMapping("/deductCouponTicket")
    public String deductCouponTicket(String couponCode) {
        String lockKey = "coupon:ticket" + couponCode;
        synchronized (this) {
            int couponTicketCount = Integer.parseInt(redisTemplate.opsForValue().get(lockKey));
            if (couponTicketCount > 0) {
                int realCouponTicketCount = couponTicketCount - 1;
                log.info("用户抢券成功,扣减优惠券数量,剩余量:" + realCouponTicketCount);
                redisTemplate.opsForValue().set(lockKey, realCouponTicketCount + "");
            } else {
                log.error("用户抢券失败,优惠券已被抢光!");
            }
        }
        return "end";
    }
}
复制代码

🤔代码思考分析:当多线程并发访问的时候,执行到synchronized(this)位置的时候,只会有一个线程获得锁,进入synchronized代码块中执行业务逻辑,当该线程执行完成之后才会释放锁,等下一个线程进来又会重新上锁执行该段代码,简单来说,通过synchronized锁机制实现多线程排队执行代码块的代码,从而保证了线程的安全。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值