一、什么是分布式锁
分布式锁是控制不同系统之间访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来确保数据一致性。
二、为什么需要分布式锁
在单机部署的系统中,一般采用线程锁来解决高并发的问题,多线程访问共享变量的问题达到数据一致性,可以使用synchornized
、ReentrantLock
等。但是对于分布式集群部署的系统架构,程序在不同的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锁机制实现多线程排队执行代码块的代码,从而保证了线程的安全。