目录
一 redis实现分布式锁
1.1 原理
利用redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
1.2 Lua脚本+hash结构实现
1.2.1 加锁命令
1.2.2 lua脚本实现加锁
1.2.3 lua脚本实现解锁
二 redis使用senx命令实现分布式锁
2.1 操作案例
1.核心代码
public void deductByRedis() {
// 加锁setnx
// 加锁,获取锁失败重试
while (!this.stringRedisTemplate.opsForValue().setIfAbsent("ljf-lock", "111")){
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//加锁成功
try {
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
else{
System.out.println("库存不足..........."+new Date());
}
} finally {
// 解锁
this.stringRedisTemplate.delete("ljf-lock");
}
}
2.controller修改调用service的方法
2.2 测试
2.2.1.启动zk
[root@localhost ] cd /root/export/apache-zookeeper-3.7.0-bin/bin
[root@localhost bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /root/export/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@localhost bin]# ./zkCli.sh
2.2.2.启动redis
[root@localhost ~]# redis-server /myredis/redis.conf
[root@localhost ~]# redis-cli -a 123456 -p 6379
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
2.2.3.启动nginx
2.2.4.启动服务
2.2.5.jemterces
1.配置
2.执行
2.2.6.查看数据库
1.初始
2.测试后
2.3 存在问题
2.3.1 死锁问题
1.存在死锁问题:
redis客户端程序执行setnx命令获取到锁之后,客户端立马宕机,导致del释放锁无法执行,进而导致锁无法释放(死锁)。采用给锁添加过期时间:
===》解决办法:给锁设置过期时间,自动释放锁。
设置过期时间两种方式:
-
通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
-
使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)
-
set key value ex 3 nx:解释:只有在key不存在,进行设置值并设置超时时间。
2.3.2 防止误删除
防误删:可能会释放其它客户端请求的锁。
场景:如果业务逻辑的执行时间是7s。执行流程如下
-
index1业务逻辑没执行完,3秒后锁被自动释放。
-
index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
-
index3获取到锁,执行业务逻辑
-
index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。
-
最终等于没锁的情况。
===》解决办法:setnx获取锁时,设置一个指定的唯一值(例如:uuid+线程id);释放前获取这个值,判断是否自己的锁
2.3.3 加锁和解锁无法保证原子性
场景:
-
index1执行删除时,查询到的lock值确实和uuid相等
-
index1执行删除前,lock刚好过期时间已到,被redis自动释放
-
index2获取了lock
-
index1执行删除,此时会把index2的lock删除。
===》解决方案:lua脚本解决删除操作缺乏原子性的问题。没有一个命令可以同时做到【判断+删除】,所有只能通过其他方式实现(LUA脚本)。Lua脚本保证释放锁,删除时保证【判断+删除】的原子性。
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end |
2.3.4 可重入性
Redis 提供了 Hash (哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的锁的重入次数,然后利用 lua 脚本判断逻辑。
a)判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby)并设置过期时间(expire)
b)如果锁被占用,则判断是否当前线程占用的(hexists),如果是则重入(hincrby)并重置过期时间(expire)
c)否则获取锁失败,后面尝试进行重试。
2.3.5 自动续期
===》解决办法:Timer定时器 + lua脚本,判断锁是否自己的锁(hexists == 1),如果是自己的锁则执行expire重置过期时间。luna脚本:
2.对应方法
2.4 redis防止误删除专题
2.4.1 场景描述&解决办法
1.问题描述:可能会释放其他服务器的锁。
进程A加锁之后,扣减库存的时间超过设置的超时时间,这里设置的锁是10秒
在第10秒的时候由于时间到期了,所以进程A设置的锁被Redis释放了(T5)
刚好进程B请求进来了,加锁成功(T6)
进程A操作完成(扣减库存)之后,把进程B设置的锁给释放了
刚好进程C请求进来了,加锁成功
进程B操作完成之后,也把进程C设置的锁给释放了
以此类推…
2.解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁。
2.5 redis原子性专题
2.5.1 描述
1.问题:删除操作缺乏原子性。
场景:
index1执行删除时,查询到的lock值确实和uuid相等
index1执行删除前,lock刚好过期时间已到,被redis自动释放
index2获取了lock
index1执行删除,此时会把index2的lock删除。
解决方案:没有一个命令可以同时做到判断+删除,所有只能通过其他方式实现(LUA脚本)
1.可重入性:hash + lua脚本 保证删除的原子性。
2..自动续期:Timer定时器 + lua脚本
解锁:先判断再删除同时保证原子性:lua脚本。
2.6 lua脚本专题
2.6.1 lua脚本
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Redis支持Lua脚本执行机制,即Redis客户端可以使用Lua语言编写脚本,由Redis服务端解析并执行,支持多线程、锁机制、运行效率高等优势。
2.6.2 redis为何要使用lua脚本
Redis中的Lua脚本可以用来完成复杂的数据处理、业务逻辑、批量操作等操作。与普通的Redis命令相比,Lua脚本具有以下几个方面的优势:
1. 原子性:Lua脚本使用Redis的单线程模型执行,可以保证脚本中的多个操作在同一时间点执行,保证了原子性。
2. 批处理和事务:Redis支持将多个命令通过Lua脚本打包成一个事务,以便保证这些命令的原子性执行。与Redis事务相比,Lua脚本的批处理和事务更加灵活和高效。
3. 自定义命令:Lua脚本可以通过定义自己的函数来实现自定义命令,可以将一些复杂的操作封装成一条简单的命令,便于使用和维护。
4.redis使用Lua脚本可以提高Redis的性能和可扩展性,提供用户更加灵活方便的编程接口和安全保障。
https://www.dbs724.com/303512.html
2.6.3 lua脚本的操作案例
在redis中需要通过eval命令执行lua脚本。命令语法:
EVAL script numkeys key [key ...] arg [arg ...] script:lua脚本字符串,这段Lua脚本不需要(也不应该)定义函数。 numkeys:lua脚本中KEYS数组的大小 key [key ...]:KEYS数组中的元素 arg [arg ...]:ARGV数组中的元素 |
案例1:EVAL "return 10" 0 ==》 10
案例2:EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 5 10 20 30 40 50 60 70 80 90
===》 10 20 60 70
案例3:EVAL "if KEYS[1] > ARGV[1] then return 1 else return 0 end" 1 10 20
====》 0
案例4:
redis.call()中的redis是redis中提供的lua脚本类库,仅在redis环境中可以使用该类库。
set ky 10 -- 设置一个ky值为10
EVAL "return redis.call('get', 'ky')" 0 ====》10
案例5:
案例6:
当call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,输出错误信息。
pcall函数不影响后续指令的执行。
注意案例中:set方法写成了sets,肯定会报错。
2.7 可重入专题
2.7.1 可重入性描述
假设X线程在a方法获取锁之后,继续执行b方法,如果此时不可重入,线程就必须等待锁释放,再次争抢锁。
锁明明是被X线程拥有,却还需要等待自己释放锁,然后再去抢锁,这看起来就很奇怪。
public synchronized void a() { b(); } public synchronized void b() { // pass } |
可重入性就可以解决这个尴尬的问题,当线程拥有锁之后,往后再遇到加锁方法,直接将加锁次数加 1,然后再执行方法逻辑。退出加锁方法之后,加锁次数再减 1,当加锁次数为 0 时,锁才被真正的释放。
可以看到可重入锁最大特性就是计数,计算加锁的次数。所以当可重入锁需要在分布式环境实现时,我们也就需要统计加锁次数。
2.7.2 redis可重入性
由于上述加锁命令使用了SETNX ,一旦键存在就无法再设置成功,这就导致后续同一线程内继续加锁,将会加锁失败。当一个线程执行一段代码成功获取锁之后,继续执行时,又遇到加锁的子任务代码,可重入性就保证线程能继续执行,而不可重入就是需要等待锁释放之后,再次获取锁成功,才能继续往下执行。
可重入:在一个synchronized修饰的同步方法和同步代码块调用本类的其他synchronized修饰的同步方法,代码块时,是永远可以得到锁的。
2.7.3 redis的Hash数据结构和命令
1.Redis的hash表结构,创建并设置值。
2.hexists只用来判断是否存在参数所指定的hash字段(集合中某个字段),只可以带一个key参数,返回值只有1(存在)和0(不存在)两种情况。例如mylock为hash的表名,uuid2为key的名。
127.0.0.1:6379> hset mylock uuid 2 (integer) 1 127.0.0.1:6379> hexists mylock uuid2 (integer) 1 127.0.0.1:6379> hexists mylock uuname (integer) 0 |
3.exists用来判断key是否存在,只有1组参数时用法和hexists一样,时间复杂度也一样,所以效率没区别。Redis3.0.3之后支持多组参数,返回存在的key的数量。
127.0.0.1:6379> set yourlock 'pa' OK 127.0.0.1:6379> exists yourlock (integer) 1 127.0.0.1:6379> exists yourklocks (integer) 0 |
4.Redis Hincrby 命令用于为哈希表中的字段值加上指定增量值。
a.这里两步到位:
redis> HSET myhash field 5 (integer) 1 redis> HINCRBY myhash field 1 (integer) 6 |
b.这里1步到位: 直接创建了hash表lock,并实现了累加统计操作为100,经过第3步,累加后为101。
127.0.0.1:6379> hincrby lock uuid 100 (integer) 100 127.0.0.1:6379> hexists lock uuid (integer) 1 127.0.0.1:6379> hincrby lock uuid 1 (integer) 101 127.0.0.1:6379> |
2.7.4 redis使用lua脚本+hash结构
redis 提供了 Hash(哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的锁的重入次数,然后利用lua脚本判断逻辑。
1.加锁逻辑:
假设值为:KEYS:[lock], ARGV[uuid, expire] 如果锁不存在或者这是自己的锁,就通过hincrby(不存在就新增并加1,存在就加1)获取锁或者锁次数加1。同时设置过期时间。
2.解锁逻辑:
1.如果自己的锁不存在,则返回nill
2.如果自己的锁存在,且可重入数-1还大于0,则还没有释放锁,返回0;
3.如果自己的锁存在,且可重入数<=0,则释放锁,删除锁,返回1
3.使用代码
2.8 自动续期
2.8.1 问题场景描述
1.问题描述:可能会释放其他服务器的锁。
进程A加锁之后,扣减库存的时间超过设置的超时时间,这里设置的锁是10秒
在第10秒的时候由于时间到期了所以进程A设置的锁被Redis释放了(T5)
刚好进程B请求进来了,加锁成功(T6)
进程A操作完成(扣减库存)之后,把进程B设置的锁给释放了
刚好进程C请求进来了,加锁成功
进程B操作完成之后,也把进程C设置的锁给释放了
以此类推…
2.解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁。
3.这种方式还是有问题;线程A还没有执行完业务,进行释放锁动作,是锁自动过期释放锁的;线程B此时获取锁,也进行业务执行,
问题1:线程A和线程B重复执行。
问题2:线程A进行释放锁时,将线程B的锁给释放了。
2.8.2 解决办法
问题1:线程A和线程B重复执行。
问题2:线程A进行释放锁时,将线程B的锁给释放了。
使用锁续期的watch dog守护程序就能解决。续锁核心代码
2.解锁核心代码
三 redis使用senx+lua脚本实现分布式锁
3.1 逻辑流程
加锁:
-
setnx:独占排他 死锁、不可重入、原子性
set k v ex 30 nx:独占排他、死锁 不可重入
-
hash + lua脚本:可重入锁
-
判断锁是否被占用(exists),如果没有被占用则直接获取锁(hset/hincrby)并设置过期时间(expire)
-
如果锁被占用,则判断是否当前线程占用的(hexists),如果是则重入(hincrby)并重置过期时间(expire)
-
否则获取锁失败,将来代码中重试
-
-
Timer定时器 + lua脚本:实现锁的自动续期
判断锁是否自己的锁(hexists == 1),如果是自己的锁则执行expire重置过期时间
解锁
-
hash + lua脚本:可重入
-
判断当前线程的锁是否存在,不存在则返回nil,将来抛出异常
-
存在则直接减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del),并返回1
-
不为0,则返回0
-
重试:递归 循环
3.2 操作步骤
1.客户端
@Component
public class DistributedLockClient {
@Autowired
private StringRedisTemplate redisTemplate;
private String uuid;
public DistributedLockClient() {
this.uuid = UUID.randomUUID().toString();
}
public DistributedRedisLock getRedisLock(String lockName){
return new DistributedRedisLock(redisTemplate, lockName, uuid);
}
}
2.分布锁
public class DistributedRedisLock implements Lock {
private StringRedisTemplate redisTemplate;
private String lockName;
private String uuid;
private long expire = 30;
public DistributedRedisLock(StringRedisTemplate redisTemplate, String lockName, String uuid) {
this.redisTemplate = redisTemplate;
this.lockName = lockName;
this.uuid = uuid + ":" + Thread.currentThread().getId();
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
return this.tryLock(-1L, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
/**
* 加锁方法
* @param time
* @param unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (time != -1){
this.expire = unit.toSeconds(time);
}
String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
" redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))){
Thread.sleep(50);
}
// 加锁成功,返回之前,开启定时器自动续期
this.renewExpire();
return true;
}
/**
* 解锁方法
*/
@Override
public void unlock() {
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
"then " +
" return nil " +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
if (flag == null){
throw new IllegalMonitorStateException("this lock doesn't belong to you!");
}
}
@Override
public Condition newCondition() {
return null;
}
// String getId(){
// return this.uuid + ":" + Thread.currentThread().getId();
// }
private void renewExpire(){
String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
new Timer().schedule(new TimerTask() {
@Override
public void run() {
if (redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
renewExpire();
}
}
}, this.expire * 1000 / 3);
}
}
3.调用
@Autowired
private DistributedLockClient distributedLockClient;
public void deductByRedisLua() {
DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lua-lock");
redisLock.lock();
//加锁成功
try {
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
else{
System.out.println("库存不足..........."+new Date());
}
//重入锁
test();
} finally {
// 解锁
redisLock.unlock();
}
}
public void test(){
DistributedRedisLock redisLock = this.distributedLockClient.getRedisLock("lua-lock");
redisLock.lock();;
System.out.println("可重入锁.....");
redisLock.unlock();
}
4.controller
3.3 测试
3.3.1.启动zk
[root@localhost ] cd /root/export/apache-zookeeper-3.7.0-bin/bin
[root@localhost bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /root/export/apache-zookeeper-3.7.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@localhost bin]# ./zkCli.sh
3.3.2.启动redis
[root@localhost ~]# redis-server /myredis/redis.conf
[root@localhost ~]# redis-cli -a 123456 -p 6379
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
3.3.3.启动nginx
3.3.4.启动服务
3.3.5.jemterces
1.配置
2.执行
3.3.6.查看数据库
1.初始
2.测试后
查看