分布式锁
- 定义:满足分布式系统或集群模式下多进程可见并且互斥的锁
- 满足条件:
多进程可见、互斥、高可用、高性能、安全性、 - 实现:分布式锁的核心是多进程之间互斥,满足这点的方式有以下三种
- 实现分布式锁时需要实现的两个基本方法:
- 获取锁:
- 互斥:确保只有一个线程获取锁
- 非阻塞:尝试一次,成功返回true,失败返回false
- 获取锁:
#添加锁,利用setnx的互斥特性
setnx lock thread1
#添加过期时间,避免服务宕机引起的死锁
expire lock 10
#添加锁,nx是互斥,ex是设置超时时间
set lock thread1 nx ex 10
fsda
- 释放锁:
- 手动释放
- 超时释放:获取锁时添加一个超时时间
#释放锁,删除即可
del key
- 分布式锁实现要满足:
- 在释放锁时存入线程标示(可以使用UUID表示)
- 在释放锁时先获取锁中的线程标示,判断是否跟当前线程标示一致
- 如果一致则释放锁
- 如果不一致则不释放锁
Redis的Lua脚本
- 在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言。Lua 教程 | 菜鸟教程 (runoob.com) 参考该网站
Redis提供的调用函数
#执行Redis命令
redis.call('命令名称','key','其他参数',……)
- 这里重点介绍Redis提供的调用函数,语法如下:
redis.call('命令名称', 'key', '其它参数', ...)
例如,我们要执行set name jack,则脚本是这样:
#执行 set name jack
redis.call('set', 'name', 'jack')
例如,我们要先执行set name Rose,再执行get name,则脚本如下:
#先执行 set name jack
redis.call('set', 'name', 'Rose')
#再执行 get name
local name = redis.call('get', 'name')
#返回
return name
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
例如,我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本,语法如下:
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
接下来我们来回一下我们释放锁的逻辑:
释放锁的业务流程是这样的
1、获取锁中的线程标示
2、判断是否与指定的标示(当前线程标示)一致
3、如果一致则释放锁(删除)
4、如果不一致则什么都不做
如果用Lua脚本来表示则是这样的:
最终我们操作redis的拿锁比锁删锁的lua脚本就会变成这样
-- 获取锁中的线程标示 get key
local id = redis.call('get', KEYS[1])
-- 比较线程标示与锁中的标示是否一致
if(id == ARGV[1]) then
-- 释放锁 del key
return redis.call('del',KEYS[1])
end
-- 不一致,直接返回
return 0
-- 简化
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
- 基于Redis的分布式锁实现思路
- 利用set nx ex 获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
- 特性:
- 利用set nx 满足互斥性
- 利用set ex 保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特征
基于setnx实现的分布式锁存在下面的问题:
- 重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
- 不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。
- 超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患
- 主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁题。
Redisson
- 是一个在Redis基础上实现的java驻内存数据网格。不仅提供了一系列的分布式的java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
- Redisson用法
1.引入依赖
2.配置Bean客户端
3.使用Redisson分布式锁
Redisson分布式锁原理:
- 可重入:利用hash结构记录线程id和重入次数
- 可重试:利用信号量和PubSub功能实现等待、唤醒、获取锁失败的重试机制
- 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间。
Redisson分布式锁主从一致性问题
- 利用redisson锁的MutiLock,将多个服务器实现主从一致
总结:- 不可重入Redis分布式锁
- 原理:利用setnx的互斥性,利用ex避免死锁;释放锁时判断线程标示
- 缺陷:不可重入、无法重试、锁超时失效
- 可重入的Redis分布式锁:
- 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待
- 缺陷:redis宕机引起锁失效问题
- 不可重入Redis分布式锁
- Redisson的multiLock:
- 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
- 缺点:运维成本高、实现复杂
Redis优化秒杀
- 思路:
a. 先利用Redis完成库存余量、一人一单的判断,完成抢单业务
b. 再将下单业务放入阻塞队列,利用独立线程异步下单 - 基于阻塞队列的异步秒杀存在哪些问题?
a. 内存限制问题
b. 数据安全问题