技术背景
首先我们需要先来了解下什么是分布式锁,以及为什么需要分布式锁。
对于这个问题,我们可以简单将锁分为两种——内存级锁以及分布式锁,内存级锁即我们在 Java 中的 synchronized 关键字(或许加上进程级锁修饰更恰当些),而分布式锁则是应用在分布式系统中的一种锁机制。分布式锁的应用场景举例以下几种:
- 互联网秒杀
- 抢优惠卷
- 接口幂等校验
文章相关视频讲解:
Linux后端开发网络底层原理知识学习点击观看:c/c++Linux后台服务器开发高级架构师学习视频资料
我们接下来以一段简单的秒杀系统中的判断库存及减库存来描述下为什么需要到分布式锁:
Copypublic String deductStock() throws InterruptedException {
// 1.从 Redis 中获取库存值
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
// 2.判断库存
if (stock > 0) {
int readStock = stock - 1;
// 3.从新设置库存
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + readStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
上面这段代码中,实现了电商系统中的一个简单小需求,即判断商品的剩余数量是否充足,充足则可以成功卖出商品,并将库存减去 1。我们很容易了解这段代码的目的。接下来我们就来一步一步地分析这段代码的缺陷。
基本实现
原子性问题
上面代码中的注释1~3部分,并没有实现原子性的逻辑。所以假设现在如果只剩下一件商品,那么可能会出现以下情况:
- 线程 A 运行到代码2,判断库存大于0,进入条件体中将 stock - 1 赋值给 readStock,在执行代码 3 前停止了下来;
- 线程 B 同样运行到代码2,判断出库存大于0(线程A并没有写回Redis),之后并没有停止,而是继续执行到方法结束;
- 线程 A 此时恢复执行,执行完代码 3,将库存写回 Redis。
现在我们就发现了问题,明明只有一件商品,却被两个线程卖出去了两次,这就是没有保证这部分代码的原子性所带来的安全问题。
那对于这个问题如何解决呢?
常规的方式自然就是加锁以保证并发安全。那么以我们 Java 自带的锁去保证并发安全,如下:
Copypublic Synchronized String deductStock() throws InterruptedException {
// 业务逻辑...
}
我们知道 synchronized 和 Lock 支持 JVM 内同一进程内的线程互斥,所以如果我们的项目是单机部署的话,到这里也就能保证这段代码的原子性了。不过以互联网项目来说,为了避免单点故障以及并发量的问题,一般都是以分布式的形式部署的,很少会以单机部署,这种情况就会带来新的问题。
分布式问题
刚刚我们将到了如果项目分布式部署的话,那么就会产生新的并发问题。接下来我们以 Nginx 配置负载均衡为例来演示并发问题,同样的请求可能会被分发到多台服务器上,那么我们刚刚所讲的 synchronized 或者 Lock 在此时就失效了。同样的代码,在 A 服务器上确实可以避免其他线程去竞争资源,但是此时 A 服务器上的那段 synchronized 修饰的方法并不能限制 B 服务器上的程序去访问那段代码,所以依旧会产生我们一开始所讲到的线程并发问题。
那么如何解决掉这个问题呢?这个是否就需要 Redis 上场了,Redis 中有一个命令SETNX key value,SETNX 是 “SET if not exists” (如果不存在,则 SET)的缩写。那么这条指令只在 key 不存在的情况下,将键 key 的值设置为 value。若键 key 已经存在,则 SETNX 命令不做任何动作。
有了上面命令做支撑,同时我们了解到 Redis 是单线程模型(不要去计较它的网络读写和备份状态下的多线程)。那么我们就可以这么实现,当一个服务器成功的向 Redis 中设置了该命令,那么就认定为该服务器获得了当前的分布式锁,而其他服务器此时就只能一直等待该服务器释放了锁为止。我们来看下代码实现:
Copy// 为了演示方便,这里简单定义了一个常量作为商品的id
public static final String PRODUCT_ID = "100001";
public String deductStock() throws InterruptedException {
// 通过 stringRedisTemplate 来调用 Redis 的 SETNX 命令,key 为商品的id,value的值在这不重要
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(RODUCT_ID, "jojo");
if (!result) {
return "error";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int readStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + readStock + "");
} else {
System.out.println("扣减失败,库存不足");
}
// 业务执行完成,删除PRODUCT_ID key
stringRedisTemplate.delete(PRODUCT_ID);
return "end";
}