目录
1、什么是分布式锁?
分布式锁是一种用于在分布式系统中同步访问共享资源的机制。在分布式系统中,多个节点同时访问共享资源可能会导致数据不一致或冲突。分布式锁通过在多个节点之间协调和管理共享资源的访问,确保同一时间只有一个节点能够访问该资源,从而保证数据的一致性和可靠性。
2、分布式锁能够解决什么问题?
并发控制:多个客户端同时访问共享资源时,分布式锁保证只有一个客户端能够对资源进行操作,避免了并发访问导致的数据冲突和竞争条件。
避免资源浪费:分布式锁可以防止多个客户端同时执行一段代码,从而避免了资源浪费和重复计算。
数据一致性:当多个客户端需要对共享数据进行更新时,分布式锁可以保证同一时间只能有一个客户端对数据进行修改,从而保持数据的一致性。
防止死锁:分布式锁可以帮助避免分布式系统中的死锁问题,确保共享资源的正常释放和使用。
防止数据竞争:分布式锁可以避免多个线程对共享资源的无序访问,保证数据的正确性和完整性。
3、写好一把锁需要的条件和刚需是什么?
1.独占性:任何时刻只能有且仅有一个线程持有
2.高可用:在Redis集群环境下,不能因为某一个节点挂了而出现获取锁失败
3.防死锁:杜绝死锁,必须要有超时控制或者撤销操作
4.不乱抢:不能私下unclock别人的锁,只能自己加锁自己解
5.可重入:同一个节点的同一个线程如果获得锁之后,它可以再次获取该锁
4、分布式锁出现超买超卖的经典案例
- 单机场景下的代码示例(有两个微服务,此时未添加分布式锁)
package com.toonyoo.redis.CloudLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class StockService {
private String _MSG = "";
private String STOCK_KEY = "stock";
// 定义lock锁
private static final Lock lock = new ReentrantLock();
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 模拟单机、高并发场景下,库存售卖的基本业务逻辑
* 调用库存售卖接口,库存减一
* @return
*/
public String sale() {
lock.lock();
try {
// 1.查询Redis库存
String result = redisTemplate.opsForValue().get(STOCK_KEY);
// 2.判断库存是否足够
if (Integer.parseInt(result) > 0){
// 3.更新库存
redisTemplate.opsForValue().set(STOCK_KEY, Integer.toString(Integer.parseInt(result) - 1));
_MSG = "库存充足,下单成功!";
}else {
return _MSG = "库存不充足,下单失败!";
}
}finally {
lock.unlock();
}
return _MSG;
}
}
高并发场景下进行压测,明显发现库存超卖,可知上述单机锁失效,原因在于Lock锁只能作用于单个JVM实例,在分布式场景下(即多个JVM)操作同一共享资源时,单机锁失效。
- 微服务场景下引入分布式锁
/**
* 模拟微服务、高并发场景下,库存售卖的基本业务逻辑,使用Redis分布式锁
* 调用库存售卖接口,库存减一
* version 2.0
*
* @return
*/
public String saleV2() {
String REDIS_KEY = "redis_lock";
// 使用uuid+线程id当做全局唯一id
String REDIS_KEY_VALUE = IdUtil.simpleUUID().toString() + Thread.currentThread().getId();
// 模拟CAS自旋操作,使用setnx命令,尝试对Redis加锁,并设置过期时间
while (!redisTemplate.opsForValue().setIfAbsent(REDIS_KEY, REDIS_KEY_VALUE, 30L, TimeUnit.SECONDS)){
// 暂停20ms,尝试再次加锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 加锁不成功,则会一直尝试加锁,若加锁成功,则会进入下方代码块
try {
// 1.查询Redis库存
String result = redisTemplate.opsForValue().get(STOCK_KEY);
// 2.判断库存是否足够
if (Integer.parseInt(result) > 0) {
// 3.更新库存
Integer stock = Integer.parseInt(result);
redisTemplate.opsForValue().set(STOCK_KEY, Integer.toString(--stock));
_MSG = "库存充足,下单成功!剩余库存" + stock;
} else {
return _MSG = "库存不充足,下单失败!剩余库存" + redisTemplate.opsForValue().get(STOCK_KEY);
}
} finally {
// 解锁操作,同时也要防止误删除 (此处代码依然会因为非原子操作而引发问题,要想真正解决得使用Lua脚本)
if (redisTemplate.opsForValue().get(REDIS_KEY).equalsIgnoreCase(REDIS_KEY_VALUE)){
redisTemplate.delete(REDIS_KEY);
}
}
return _MSG;
}
5、Redisson分布式锁引入
红锁算法(Redlock Algorithm)是一种用于分布式系统中实现分布式锁的算法。它由Redis的作者提出,用于解决分布式环境下的并发访问问题。
红锁算法的基本原理如下:
-
获取锁:当一个客户端需要获取锁时,它会向多个Redis节点(通常是3个或5个)发送SET命令,这3或5个Redis节点均是master,它们之间没有任何关系,每个命令都设置一个相同的键和值,并附带一个超时时间。
-
大部分节点都成功设置锁:如果大多数节点(例如大于等于N/2+1,其中N是Redis节点的总数)都能成功设置锁,那么该客户端就会认为自己成功获取到了锁。
-
获取锁失败:如果大多数节点设置锁失败,那么客户端认为自己没有获取到锁,并放弃当前的操作。
-
锁的释放:当客户端释放锁时,它会向所有节点发送DEL命令进行解锁。
Java代码实现Redisson分布式锁
1、引入pom依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.24.3</version>
</dependency>
2、RedisConfig
package com.toonyoo.config;
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Redis相关配置
*/
@Configuration
public class RedisConfig {
/**
* Redisson配置方法
* @return
*/
@Bean
public Redisson getRedisson() {
// 创建config对象
Config config = new Config();
// Redis单节点,配置数据库地址,选择几号库,数据库连接密码等
config.useSingleServer().setAddress("redis://192.168.10.128:6379").setDatabase(0).setPassword("11111");
// Redis多节点配置
// config.useClusterServers().addNodeAddress("redis://192.168.10.126:6379","redis://192.168.10.127:6379","redis://192.168.10.128:6379");
return (Redisson) Redisson.create(config);
}
}
3、业务代码之Redisson分布式锁的运用
@Autowired
private Redisson redisson;
public String saleByRedisson() {
String REDIS_KEY = "redis_lock";
RLock redissonLock = redisson.getLock(REDIS_KEY);
redissonLock.lock(20,TimeUnit.MILLISECONDS);
try {
// 1.查询Redis库存
String result = redisTemplate.opsForValue().get(STOCK_KEY);
// 2.判断库存是否足够
if (Integer.parseInt(result) > 0) {
// 3.更新库存
Integer stock = Integer.parseInt(result);
redisTemplate.opsForValue().set(STOCK_KEY, Integer.toString(--stock));
_MSG = "库存充足,下单成功!剩余库存" + stock;
} else {
return _MSG = "库存不充足,下单失败!剩余库存" + redisTemplate.opsForValue().get(STOCK_KEY);
}
} finally {
// 解锁操作,同时也要防止误删除(只有当 当前锁被占有并且是当前所属线程占有才能够解锁,此处方法底层使用了Lua脚本封装,可保证原子性操作)
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
}
return _MSG;
}