分布式锁(Redis)
分布锁是分布式系统中重要的一环,在多线程的场景下,就会存在并发问题,这时加锁来保证线程安全,在之前我们也使用过锁,之前我们使用的锁是JVM层面的锁,它只能在一个JVM内存中才能起作用,而在分布式系统环境,我们使用的往往是多个JVM,这时普通的锁即JVM层面的锁就会失效,这时我们就要采用分布式锁,可保证集群模式下多进程可见并且互斥的锁,是进程级别的锁。分布式锁的实现有很多种方法,本文介绍的是通过Redis来实现分布式锁。
普通锁的示意图:只能在单个JVM有效。
分布锁示意图:在多个JVM间也有效。
分布式锁需要满足的条件
实现分布式锁几种方式的对比
MySQL | Redis | Zookeeper | |
---|---|---|---|
互斥 | 利用MySQL本身的互斥锁机制 | 利用setnx互斥命令 | 利用节点唯一性和有序性实现互斥 |
高性能 | 好 | 好 | 好 |
高可用 | 一般 | 好 | 一般 |
安全性 | 断开连接,自动释放锁 | 利用超时时间,到期释放 | 临时节点,断开连接自动释放 |
获取锁
#不存在就可以设置成功
setnx lock thread
#添加锁的超时时间,当出现故障时可以超时释放
expire lock 10
#上面两个命令合成一条命令,保证两个命令的原子性
set lock thread nx ex 10
释放锁
#删除key
del key
自定义定义获取锁和释放锁方法
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.6.5version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<version>2.6.5version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>2.5.2version>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>4.2.3version>
dependency>
<dependency>
<groupId>org.code-house.eaio-uuidgroupId>
<artifactId>uuidartifactId>
<version>3.4.1version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.1.1version>
dependency>
dependencies>
server:
port: 8888
spring:
redis:
host: 192.168.247.128
port: 6379
package com.qiumin.lock;
/**
* @author qiumin
* @classname ILock
* @Description love code
* @date 2022-11-14 17:21
*/
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 超时时间
* @return 返回true获取锁成功,返回false获取失败
* */
boolean tryLock(long timeoutSec);
/**
* 释放锁
* */
void unlock();
}
package com.qiumin.utils;
import com.qiumin.lock.ILock;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author qiumin
* @classname SimpleRedisLock
* @Description love code
* @date 2022-11-14 17:25
*/
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
//锁前缀
private static final String KEY_PREFIX="lock:";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取锁
long threadId = Thread.currentThread().getId();
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//释放锁
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
package com.qiumin.controller;
import com.qiumin.utils.SimpleRedisLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author qiumin
* @classname RedisController
* @Description love code
* @date 2022-11-14 16:58
*/
@RestController
public class RedisController {
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@RequestMapping("/to")
public void testRedis(){
redisTemplate.opsForValue().set("blue","666");
System.out.println(redisTemplate.opsForValue().get("blue"));
}
@RequestMapping("/testLock")
public String testRedisDIY(){
SimpleRedisLock lock = new SimpleRedisLock("order", stringRedisTemplate);
boolean isLock = lock.tryLock(20);
if(!isLock){ //获取锁
System.out.println("获取锁失败!!!");
}
try{
System.out.println("业务处理中...");
}finally {
//释放锁
lock.unlock();
}
return "ok";
}
}
package com.qiumin;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.Jedis;
/**
* @author qiumin
* @classname TestRedis
* @Description love code
* @date 2022-11-14 15:40
*/
@SpringBootTest
public class TestRedis {
@Autowired
RedisTemplate redisTemplate;
@Test
public void testRedis(){
Jedis jedis = new Jedis("192.168.247.128", 6379);
String ping = jedis.ping();
jedis.set("dex","连接成功");
System.out.println(jedis.get("dex"));
}
}
说明: 当第一个线程设置了key时,后面的线程就不能设置值了即获取锁失败,key的时间超时key自动失效即释放锁(删除key),其他线程才能获取锁。
上面的代码可能存在误删的问题
解决方法:在释放锁时取出key中的value判断value中的标识(可用uuid+线程id标识)是否为当前线程的,是则释放。
v2.0版本,由于判断和释放是两个操作不是原子性,所有也存在误删,,但几率小,后面利用lua脚本保证两个操作的原子性
package com.qiumin.utils;
import cn.hutool.core.lang.UUID;
import com.qiumin.lock.ILock;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author qiumin
* @classname SimpleRedisLock
* @Description love code
* @date 2022-11-14 17:25
*/
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
//锁前缀
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取锁,uuid+线程id 作为标识
String threadId = ID_PREFIX+Thread.currentThread().getId();
//key不存在是才可以设置key值
Boolean success = stringRedisTemplate.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 = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if(threadId.equals(id)){
//释放锁
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
}
lua脚本是由lua编程语言编写的,lua脚本可以保证Redis两个操作的院子性
redis提供了调用函数:
#格式
redis.call('命令名称','key','其他参数',...)
#例子:执行 set name qiumin
redis.call('set','name','qiumin')
#获取用local声明的变量name存储
local name = redis.call('get','name')
#返回
return name
#执行脚本
EVAL "return redis.call('set','name','qiumin')" 0
#脚本中可以从这两个数组获取参数,在lua脚本中数组下标从1开始
EVAL "return redis.call('set',keys[1],argv[1])" 1 name qiumin
利用lua脚本保证判断锁标识和释放锁 两个操作的原子性
--获取锁中的线程标识 get key
local id = redis.call('get',KEYS[1])
--比较线程标识与锁中标识是否一致
if(id == ARGV[1]) then
--释放锁
return redis.call('del',KEYS[1])
end
return 0
利用RedisTemplate调用lua脚本,execute方法
@Nullable
public <T> T execute(RedisCallback<T> action) {
return this.execute(action, this.isExposeConnection());
}
单独编写lua脚本文件可以减少lua语言与java代码的耦合,idea下载 Emmylua插件
--获取锁中的线程标识 get key
local id = redis.call('get',KEYS[1])
--比较线程标识与锁中标识是否一致
if(id == ARGV[1]) then
--释放锁
return redis.call('del',KEYS[1])
end
return 0
package com.qiumin.utils;
import cn.hutool.core.lang.UUID;
import com.qiumin.lock.ILock;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @author qiumin
* @classname SimpleRedisLock
* @Description love code
* @date 2022-11-14 17:25
*/
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
//锁前缀
private static final String KEY_PREFIX="lock:";
private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
//lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
//加载该类时就将lua脚本文件读入
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
//获取锁,uuid+线程id 作为标识
String threadId = ID_PREFIX+Thread.currentThread().getId();
//key不存在是才可以设置key值
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//调用Lua脚本
stringRedisTemplate.execute(UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX+Thread.currentThread().getId()
);
}
基于redia实现分布式锁的思路
特性
基于setnx实现分布式锁存在下列问题
Redisson概述
Redisson是一个在Redis的基础上实现的Java驻内存数据网格,提供了一系列的分布式的java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。如:可重入锁
、公平锁
、联锁
、红锁
、读写锁
、闭锁
。
官方网址: https://redisson.org
GitHub地址: https://github.com/redisson/redisson
使用redisson实现分布式锁
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.6version>
dependency>
package com.qiumin.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author qiumin
* @classname RedissonConfig
* @Description love code
* @date 2022-11-14 22:37
*/
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
//连接redis
config.useSingleServer().setAddress("redis:192.168.247.128:6379");
//返回创建的对象
return Redisson.create(config);
}
}
package com.qiumin.controller;
import com.qiumin.utils.SimpleRedisLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author qiumin
* @classname RedisController
* @Description love code
* @date 2022-11-14 16:58
*/
@RestController
public class RedisController {
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
//注入RedissonClient
RedissonClient redissonClient;
@RequestMapping("/to")
public void testRedis(){
redisTemplate.opsForValue().set("blue","888");
System.out.println(redisTemplate.opsForValue().get("blue"));
}
@RequestMapping("/testLock")
public String testRedisDIY(){
//使用Redisson获取锁
RLock lock = redissonClient.getLock("order");
boolean isLock = lock.tryLock();
if(!isLock){ //获取锁
System.out.println("获取锁失败!!!");
}
try{
System.out.println("业务处理中...");
}finally {
//释放锁
lock.unlock();
}
return "ok";
}
}
tryLock()方法的参数解析: (参数可以都为空)
基于redis的setnx的分布式锁不支持锁的重入,因为只有在key不存在时才能设置,存在就不能设置,这时需要用到Redisson
获取锁的lua脚本
local key = KEYS[1] ; --锁的key
local threadId = ARGV[1]; --线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间判断是否存在
if(redis.call('exists',key) == 0) then
--不存在,获取锁
redis.call( 'hset',key,threadId, '1);
--设置有效期
redis.call('expire',key,releaseTime) ;
return 1; --返回结果
end;
--锁已经存在,判断threadId是否是自己
if(redis.call('hexists',key,threadId) == 1) then
--存在,获取锁,重入次数+1
redis.call('hincrby',key,threadId, '1');
redis.call('expire',key,releaseTime); --设置有效期
return 1; --返回结果
end ;
return 0; --代码走到这里,说明获取锁的不是自己,获取锁失败
释放锁的lua脚本
local key = KEYS[1]; --锁的key
local threadId = ARGV[1]; --线程唯一标识
local releaseTimeARGV[2] ;--锁的自动释放时间
--判断当前锁是否还是被自己持有
if (redis.call( 'HEXISTS', key, threadId) == 0) then
return nil; --如果已经不是自己, 则直接返回
end;
--是自己的锁,则重入次数- 1
local count = redis. call( 'HINCRBY',key, threadId, -1);
--判断是否重入次数是否已经为0
if (count > 0) then
--大于0说明不能释放锁,重置有效期然后返回
redis.call('EXPIRE', key, releaseTime) ;
return nil ;
eLse --等于0 说明可以释放锁,直接删除
redis.call('DEL',key);
return nil;
end;
当调用lock.tryLock()方法时传入了重试等待时间时就会在等待时间段内进行获取锁的重试
重试的基本原理: 发布订阅
、信号量机制
看门狗机制是redisson保证锁超时释放和持续续约的机制,当调用lock.tryLock()方法时没有指定超时释放时间时,就会默认采用看门狗机制
示意图:
Redisson分布式锁原理
qiumin