分布式锁介绍
分布式锁:满足分布式系统或者集群环境下多进程可见并且互斥的锁。
特性 | MySQL | Redis | Zookeeper |
---|---|---|---|
互斥 | 利用mysql本身的互斥锁机制 | 利用setnx这样的互斥命令 | 利用节点的唯一性和有序性实现互斥 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用锁超时时间,到期释放 | 临时节点,断开连接自动释放 |
分布式锁初级版本
设计锁对象
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功;false代表获取锁失败
*/
boolean tryLock(long timeoutSec);
/**
* 释放锁
*/
void unlock();
}
public class SimpleRedisLock implements ILock {
private String name;
private RedisTemplate redisTemplate;
public SimpleRedisLock(String name, RedisTemplate redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
Long ThreadId = BaseContext.getCurrent().getId();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+ name, ThreadId+"", timeoutSec, TimeUnit.SECONDS);
// 如果 flag 为 true,返回 true;如果 flag 为 false 或 null,返回 false。
return BooleanUtil.isTrue(flag);
}
@Override
public void unlock() {
redisTemplate.delete(KEY_PREFIX+ name);
}
}
使用锁解决一个用户的并发抢券操作
Long userId = BaseContext.getCurrent().getId();
/**
* 每一个请求过来,这个id对象都是一个全新的id对象,因为要是对userId加锁的话,对象变了锁就变了,那不行
* 我们希望id的值一样,所以用了toString(),但是toString()依旧不能保证是对对象的值加锁的
* toString底层是new 一个String数组,还是new了一个新对象,同一个用户id在不同的请求中过来,每次都new一个,还是不能把锁加载同一个用户上
* 于是用intern() ,intern()方法可以去字符串常量池中找字符串值一样的引用返回
* 这样一来,如果你的userId是5,不管你new了多少个字符串,只要值是一样的,返回的结果也一样。这样就可以锁住同一个用户
* 不同的用户不会被锁住
*/
SimpleRedisLock lock = new SimpleRedisLock("order:"+userId, redisTemplate);
boolean isLock = lock.tryLock(1200); // 获取锁
if (!isLock) {
// 获取锁失败, 返回或者重试
return Result.fail("不允许重复下单");
}
try{
// 获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); // 拿到当前对象的代理对象,其实就是IVoucherOrderService这个接口的代理对象,返回的是Object,做个强转
return proxy.createVoucherOrder(voucherId); // 如果报错了是因为我们的接口中没有这个方法,那我们就在接口中创建一下这个方法就行
}finally {
lock.unlock();// 释放锁
}
存在问题:
解决方法:释放锁的时候看是不是自己获取的锁
解决释放的锁不是自身原先获取的锁
- 在获取锁时显示线程序示(可以用UUID表示)
- 在释放锁时先获取锁中的线程序示,判断是否与当前线程序示一致
- 如果一致则释放锁
- 如果不一致则不释放锁
锁对象里再设置,添加释放锁的身份确认
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取线程标识
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标识
String id = (String) redisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标识是否一致
if (threadId.equals(id)) {
// 释放锁
redisTemplate.delete(KEY_PREFIX + name);
}
}
Redis的Lua脚本
为什么使用 Lua 脚本?
-
原子性:Lua 脚本在 Redis 中执行时是原子的,即脚本在执行期间不会被其他命令中断。
-
减少网络开销:将多个操作合并为一个脚本,减少客户端与服务器之间的通信次数。
-
复杂操作:Lua 脚本支持条件判断、循环等复杂逻辑,适合处理需要多个步骤的操作。
基本用法
-
EVAL:执行 Lua 脚本。
EVAL "return 'Hello, Redis!'" 0
其中,
"return 'Hello, Redis!'"
是 Lua 脚本,0
表示没有键参数。 -
SCRIPT LOAD:加载脚本到 Redis,返回 SHA1 校验和。
SCRIPT LOAD "return 'Hello, Redis!'"
返回的 SHA1 值可用于后续的
EVALSHA
命令。 -
EVALSHA:通过 SHA1 值执行已加载的脚本。
EVALSHA <SHA1> 0
3. 脚本中的 Redis 命令
在 Lua 脚本中,可以通过 redis.call()
或 redis.pcall()
调用 Redis 命令。
-
redis.call()
:执行命令,出错时抛出异常。 -
redis.pcall()
:执行命令,出错时返回错误信息。
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
-
KEYS[1]
是键参数,ARGV[1]
是值参数。 -
1
表示有 1 个键参数。
4. 脚本的原子性
Lua 脚本在执行期间,Redis 不会处理其他命令,确保脚本的原子性。但长时间运行的脚本可能导致 Redis 阻塞,需谨慎使用。
5. 脚本的缓存
Redis 会缓存加载的脚本,通过 SCRIPT LOAD
加载的脚本会一直保留,直到服务器重启或使用 SCRIPT FLUSH
清除。
6. 错误处理
-
使用
redis.pcall()
捕获错误。 -
脚本中的语法错误会在加载时检测到,运行时错误则会在执行时抛出。
Lua脚本实现释放分布式锁
-- 这里的 KEYS[1] 就是传入的key,这里的 ARGV[1] 就是当前传递的参数值
-- 获取当前的值,判断是否与当前传递的参数一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除
return redis.call('DEL', KEYS[1])
end
-- 不一致,则返回0
return 0
分布式锁Redisson
Redisson 是一个用于 Java 的 Redis 客户端,它不仅提供了对 Redis 数据库的简单 API 接口,还提供了许多高级功能,旨在简化分布式应用程序的开发。它又以下的特性:
-
丰富的数据结构:提供了多种高级数据结构,如映射、集合、列表等,兼容 Java 集合框架。
-
分布式执行:支持分布式任务处理,实现高并发的任务执行。
-
分布式锁:确保在分布式环境下对共享资源的安全访问。
-
对象映射:自动序列化和反序列化 Java 对象,简化数据存取。
-
反应式编程支持:适合高并发和低延迟的应用程序。
-
高可用性:支持 Redis Sentinel 和 Redis Cluster,确保稳定运行。
使用步骤
导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
配置redisson的配置类
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
// 配置
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379"); // 使用正确的地址格式
// 创建RedissonClient对象
return Redisson.create(config);
}
}
使用Redisson
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
// 获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
// 尝试获取锁,参数分别是:获取锁的最大等待时间(单位是时间尝试),锁自动释放的时间,时间单位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); // 无参的话就是不等待,30秒自动释放
// 判断释放锁获取成功
if (isLock) {
try {
System.out.println("执行业务");
} finally {
// 释放锁
lock.unlock();
}
}
}
Redis消息队列实现异步秒杀
消息队列(Message Queue),字面意思是存放消息的队列。最简单的消息队列模型包括三个角色:
- 消息队列:存储和管理消息,也被称为消息中介(Message Broker)
- 生产者:发送消息到消息队列
- 消费者:从消息队列获取消息并处理消息
Redis 提供了三种不同的方式来实现消息队列:
- list结构:基于 List 结构的消息队列
- PubSub:基本的点对点消息模型
- Stream:相对完善的消息队列模型