前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。
基础篇:
进阶篇:
接上期内容:上期完成了手写分布式锁的相关知识学习。下面学习RedLock(红锁)分布式锁(官方推荐),话不多说,直接发车。
一、RedLock定义
(一)、官方说明
官方链接:Distributed Locks with Redis | Docs
(二)、为啥学习?怎么产生
学习原因:假设线程1首先获取锁成功,将键值对写入 redis 的 master 节点,在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;redis 触发故障转移,其中一个 slave 升级为新的 master,此时新上位的master并不包含线程1写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁,此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。
(三)、RedLock设计理念
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁客户端执行以下操作:
1 | 获取当前时间,以毫秒为单位; |
2 | 依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁; |
3 | 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功; |
4 | 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。 |
5 | 如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。 |
二、RedLock落地实现(Redisson)
(一)、单机
①、导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.45.0</version>
</dependency>
②、编码实现
步骤省略,最终目录图:
SingleRedLockConfig类:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.Redisson;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SingleRedLockConfig {
@Bean
public Redisson redisson() {
Config config = new Config();
// 单机模式
config.useSingleServer().setAddress("redis://192.168.40.128:6379").setPassword("root");
// 设置 JSON 序列化编解码器
config.setCodec(new JsonJacksonCodec(new ObjectMapper()));
return (Redisson) Redisson.create(config);
}
}
SingleRedLockDemoController类:
import com.nb.redisdemo.RedLockDemo.SinglePlayerDemo.service.SingleRedLockDemoService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SingleRedLockDemoController {
@Resource
private SingleRedLockDemoService singleRedLockDemoService;
@GetMapping(value = "/inventory/saleByRedisson")
public String sale()
{
return singleRedLockDemoService.saleByRedisson();
}
}
SingleRedLockDemoService类:
import jakarta.annotation.Resource;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class SingleRedLockDemoService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
@Resource
private Redisson redisson;
public String saleByRedisson() {
String retMessage = "";
String key = "redLock";
RLock redissonLock = redisson.getLock(key);
redissonLock.lock();
try {
//1 查询库存信息
String result = stringRedisTemplate.opsForValue().get("inventory001");
//2 判断库存是否足够
int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
//3 扣减库存
if (inventoryNumber > 0) {
stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber;
System.out.println(retMessage);
} else {
retMessage = "商品卖完了,o(╥﹏╥)o";
}
} finally {
// 如果是本线程且是获取锁成功,才可以解锁
if (redissonLock.isHeldByCurrentThread() && redissonLock.isLocked()) {
redissonLock.unlock();
}
}
return retMessage + "\t" + "服务端口号:" + port;
}
}
③、测试功能
单机测试:访问localhost:8088/inventory/saleByRedisson进行测试,测试结果:
Nginx + Jmeter压测:访问自己Nginx服务器IP+/inventory/saleByRedisson,测试结果:
单机测试通过。
(二)、多机
由于硬件关系,三台存放redis锁的机器部署在同一台服务器上,三台机器互不从属,都是master
①、修改yaml文件
##==========================RedLock分布式锁配置=====================
spring.redis.database=0
spring.redis.password=root
spring.redis.timeout=3000
spring.redis.mode=single
spring.redis.pool.conn-timeout=3000
spring.redis.pool.so-timeout=3000
spring.redis.pool.size=10
spring.redis.single.address1=服务器ip:6379
spring.redis.single.address2=服务器ip:6380
spring.redis.single.address3=服务器ip:6381
②、编码实现
由于RedLock这个对象已经被遗弃了,所以使用MultiLock实现。
步骤省略,最终项目目录:
MultiMachineRedLockConfig类:
import jakarta.annotation.Resource;
import org.apache.commons.lang.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class MultiMachineRedLockConfig {
@Resource
RedisProperties redisProperties;
@Bean
RedissonClient redissonClient1() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress1();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient2() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress2();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
@Bean
RedissonClient redissonClient3() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress3();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
}
RedisPoolProperties类:
import lombok.Data;
@Data
public class RedisPoolProperties {
private int maxIdle;
private int minIdle;
private int maxActive;
private int maxWait;
private int connTimeout;
private int soTimeout;
/**
* 池大小
*/
private int size;
}
RedisProperties类:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperties {
private int database;
/**
* 等待节点回复命令的时间。该时间从命令发送成功时开始计时
*/
private int timeout;
private String password;
private String mode;
/**
* 池配置
*/
private RedisPoolProperties pool;
/**
* 单机信息配置
*/
private RedisSingleProperties single;
}
RedisSingleProperties类:
import lombok.Data;
@Data
public class RedisSingleProperties {
private String address1;
private String address2;
private String address3;
}
MultiMachineRedLockDemoController类:代码参考来源:Distributed locks and synchronizers - Redisson Reference Guide
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.RedissonMultiLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@Slf4j
public class MultiMachineRedLockDemoController {
public static final String CACHE_KEY_RED_LOCK = "multiLock";
@Resource
RedissonClient redissonClient1;
@Resource
RedissonClient redissonClient2;
@Resource
RedissonClient redissonClient3;
@GetMapping(value = "/multiLock")
public String getMultiLock() throws InterruptedException {
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
String uuidValue = uuid + ":" + Thread.currentThread().getId();
RLock lock1 = redissonClient1.getLock(CACHE_KEY_RED_LOCK);
RLock lock2 = redissonClient2.getLock(CACHE_KEY_RED_LOCK);
RLock lock3 = redissonClient3.getLock(CACHE_KEY_RED_LOCK);
RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3);
redLock.lock();
try {
System.out.println(uuidValue + "\t" + "---come in biz multiLock");
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(uuidValue + "\t" + "---task is over multiLock");
} catch (Exception e) {
e.printStackTrace();
log.error("multiLock exception ", e);
} finally {
redLock.unlock();
log.info("释放分布式锁成功key:{}", CACHE_KEY_RED_LOCK);
}
return "multiLock task is over " + uuidValue;
}
}
③、测试功能
访问localhost:8088/multiLock,测试结果:
三台redis客户端以及程序后台都正常,测试通过。
三、Redisson源码分析
参考上章学习手写分布式锁的案例。看看Redisson是如何实现redis分布式锁的。
①、加锁
1、redisson新建锁的默认时间为30秒
2、尝试获取锁
如果锁不存在,则通过hset设置它的值,并设置过期时间
如果锁已存在,并且锁的是当前线程,则通过hincrby给数值递增1。
如果锁已存在,但并非本线程,则返回过期时间。
3、加锁成功后,开启监控(自动续期)
每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始。
②、解锁
四、总结
RedLock 作为分布式锁的优秀解决方案,以其独特的设计理念和可靠的实现方式,为分布式系统中的数据一致性和并发控制提供了有力支持。从单机到多机的实现过程,我们看到了其在不同场景下的应用灵活性。通过对 Redisson 源码的分析,进一步了解了如何将 RedLock 的理论落地为高效的代码实践。在实际开发中,合理运用 RedLock 能够有效解决分布式系统中的诸多同步难题,提升系统的稳定性与可靠性,是每一位分布式系统开发者值得深入掌握的关键技术。
ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。