为什么要用分布式锁
假设你现在正在浏览你们学校的二手物品交易网站,你看到了大四学长正在拍卖的一本全新的《Java 编程思想》,你心动了,下单,支付,库存减 1,等待收货。但是,你有没有考虑这样一个问题,你隔壁寝室的哥们也看中了这本《Java 编程思想》,他和你执行着一样的流程,你俩同时完成支付,假如你们这个网站的开发人员没有做并发处理,让你们两个都买到了这本书,但总共只有一本,那这本书到底该归谁。当然,这个假设发生的概率极低。如果要彻底避免这样的情况,开发人员只要在减少库存对应的代码块上加一个 synchronized 关键字,加一个锁,这块代码同时只有一个线程可以访问,那便可解决这个问题。
对于学校内部,这种流量小,并发量低的单体应用,这样操作无疑可以解决问题。但是,对于像淘宝,京东这类大型的电商网站,它们采用分布式的系统架构,尤其在双 11,618 这种购物狂欢节,如此高并发的情况下,单单对代码加一个同步锁是绝对不能解决问题的,它们要保证整个分布式系统的数据一致性,我们的分布式锁应运而生。
什么是分布式锁
分布式锁也是一把锁,用来锁住共享资源的,比如库存这一资源,大型电商网站,单单处理库存这一个服务就会部署到成千上万台机器上,库存就属于共享资源,我们用分布式锁来保证共享资源的一致性。简单地说,就是一个线程开始执行处理库存的代码后,他就会持有一把锁,关起门来,自己偷偷处理,处理完后,开锁,走人,也就是释放锁,释放了的锁可以被其他线程持有,这把锁就是分布式锁,这样就保证了在同一时间,只有一个线程可以操作库存,保证了分布式系统的数据一致性。
怎样实现分布式锁
实现分布式锁的方式有很多,MySQL、ZooKeeper、Redis 等都能实现分布式锁,本文来探讨一下如何用 Redis 实现分布式锁。
分布式锁实现的思路很简单,就是进来一个线程先占位,当别的线程进来操作时,发现已经有人占位了,就会放弃或者稍后再试。
在 Redis 中,占位一般使用 setnx 指令,先进来的线程先占位,线程的操作执行完成后,再调用 del 指令释放位置。同时为了防止死锁,我们一般还要给锁加一个过期时间,到期了自动释放。
下面看第一种实现方式
@GetMapping("/deduct_Stock")
public String deductStock() {
// 给每一把锁分配不同的值
String clientId = UUID.randomUUID().toString();
try {
// 获得锁
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", clientId, 10, TimeUnit.SECONDS);
// 业务代码
if (!result) {
return "error";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + stringRedisTemplate.opsForValue().get("stock"));
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
// 释放锁
if (clientId.equals(stringRedisTemplate.opsForValue().get("lockKey"))) {
stringRedisTemplate.delete("lockKey");
}
}
return "success";
}
Spring Boot 为我们提供了 StringRedisTemplate 来操作 Redis,StringRedisTemplate 为我们提供的opsForValue().setIfAbsent() 方法就相当于 Redis 的 setnx 命令,表示当 key 不存在的时候进行 set。
当线程执行到 deductStock() 方法的时候,首先会获得一把锁,业务执行完毕之后要释放锁,因为当业务执行过程中可能会抛出异常,所以将删除锁的操作放在 finally 中。
因为我们设置锁的时间是 10 s,如果业务执行之间大于 10 s,锁就会失效,第二个线程会获得锁,这是为了防止第一个线程执行结束的时候误删第二个线程的锁,就需要在删除的时候进行判断,我们给每一把锁设置一个 UUID 的值,这样可以做到在删除的时候判断是否是自己的锁。
上面的代码写着还是蛮长的,那么有没有简单一点的办法呢?当然是有的!那就是 Redisson。Redisson 对 Redis 请求做了较多的封装,对于锁,也提供了对应的方法可以直接使用。
第二种实现方式
@GetMapping("/deduct_Stock")
public String deductStock() {
Config config = new Config();
// 配置 Redis 基本连接信息
config.useSingleServer().setAddress("redis://47.104.218.84:6379");
// 获取一个 RedissonClient 对象
RedissonClient redisson = Redisson.create(config);
// 获取一个锁对象实例
RLock redissonLock = redisson.getLock("lockKey");
try {
// 设置超时时间
redissonLock.lock(30, TimeUnit.SECONDS);
// 业务代码
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + stringRedisTemplate.opsForValue().get("stock"));
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
// 释放锁
redissonLock.unlock();
}
return "success";
}
Redisson 分布式锁实现原理
从图中我们可以看到,一个线程获得锁之后,会开启一个后台线程,每 10 s 检查是否还持有锁,如果持有则延长锁的时间。当线程从 Master 获得锁之后,Master 需要将锁的信息同步到 Slave 上,如果在同步的过程中 Master down 掉,Slave 重新选举新的 Master,新选举的 Master 中没有锁的信息,所以用 Redis 做分布式锁天生性能是不高的。