在第一种解决方案的基础上,可能部分人会相到,既然主动删除key可能会出现异常情况,那么就设值key的过期时间到期自动删除。
127.0.0.1:6379> setnx lock true
(integer) 1
127.0.0.1:6379> expire lock 10 # 设值过期时间10s
(integer) 1
127.0.0.1:6379> setnx lock true # 10s内再次设值失败
(integer) 0
127.0.0.1:6379> setnx lock true # 10skey过期,后设置成功
(integer) 1
这种的方案和前面的方案其实并没有本质上的区别,它还是可能会出现服务器异常等情况,导致expire的不到执行的情况,换汤不换药,如下图所示:
2.2.3 原子操作
基于上面两种方案,我们可以发现,产生问题的本质在于两个操作并不是原子操作。方案一中是setnx指令加一个del指令,方案二中是setnx指令加一个expire指令,这两个指令并不是原子指令。基于这个问题,Redis官方将这两个指令组合在了一起,解决Redis分布式锁原子性操作的问题。
先认真看set指令可选参数 EX 和 NX
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds :将键的过期时间设置为 seconds 秒。执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
PX milliseconds :将键的过期时间设置为 milliseconds 毫秒。执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
NX:只在键不存在时,才对键进行设置操作。执行 SET key value NX 的效果等同于执行 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
127.0.0.1:6379> set lock true EX 10 NX # 设置 10s生效
OK
127.0.0.1:6379> set lock true EX 10 NX # 10s内再次设值失败
(nil)
127.0.0.1:6379> set lock true EX 10 NX # 10s后设置成功
OK
如上这个操作就成功的解决了Redis分布式锁的原子操作问题。
2.2.4 解锁
Redis分布式锁加锁在上面讲述了,而Redis分布式锁的解锁过程其实就是将key删除,key的删除有客户端调用del指令删除,也有设置key的过期时间自动删除。但是这个删除不能乱删除,不能说客户端A请求的锁被客户端B给删除了……,那这把锁就是一把烂锁了。
为了防止客户端A请求的锁被客户端B给删除了这种情况,我们通过匹配客户端传入的锁的值与当前锁的值是否相等来做判断(这个值是随机且保证不会重复的),如果相等就删除,解锁成功。
但是Redis并未提供这样的功能,我们只能通过Lua脚本来处理,因为Lua脚本可以保证多个指令的原子性执行。
示例:
首先设置一个key,这个key的值是123456789,通过客户端传入的value值是否相等来校验是否允许删除这个key
127.0.0.1:6379> get lock
(nil)
127.0.0.1:6379> set lock 123456789 # 设置一个key 值为123456789
OK
127.0.0.1:6379> get lock
“123456789”
在客户机上编写lua脚本,lock.lua文件,文件内容如下
if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])
else
return 0
end
测试通过错误的value值去执行lua脚本,这个时候删除key失败,返回0
通过正确的value值执行则返回1,说明key被删除了。
2.2.5 代码实现
一下演示一个spring boot项目来实现Redis分布式锁,为了方便大家使用,我贴出的代码比较全面,篇幅稍多。
pom依赖
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
org.springframework.boot
spring-boot-starter-web
redis.clients
jedis
3.0.1
org.projectlombok
lombok
cn.hutool
hutool-all
5.3.4
Redis配置文件
package com.lizba.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
-
-
Redis简单配置文件
-
@Author: Liziba
-
@Date: 2021/7/11 11:17
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
protected static final Logger logger = LoggerFactory.getLogger(RedisConfig.class);
@Value(“${spring.redis.host}”)
private String host;
@Value(“${spring.redis.port}”)
private int port;
@Value(“${spring.redis.jedis.pool.max-active}”)
private int maxTotal;
@Value(“${spring.redis.jedis.pool.max-idle}”)
private int maxIdle;
@Value(“${spring.redis.jedis.pool.min-idle}”)
private int minIdle;
@Value(“${spring.redis.password}”)
private String password;
@Value(“${spring.redis.timeout}”)
private int timeout;
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);
logger.info(“JedisPool注入成功!!”);
logger.info(“redis地址:” + host + “:” + port);
return jedisPool;
}
}
application.yml配置文件
server:
port: 18080
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
timeout: 10000
password:
jedis:
pool:
max-active: 20
max-idle: 20
min-idle: 0
获取锁与释放锁代码
package com.lizba.utill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
-
-
Redis分布式锁简单工具类
-
@Author: Liziba
-
@Date: 2021/7/11 11:42
*/
@Service
public class RedisLockUtil {
private static Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);
/**
- 锁键 -> key
*/
private final String LOCK_KEY = “lock_key”;
/**
- 锁过期时间 -> TTL
*/
private Long millisecondsToExpire = 10000L;
/**
- 获取锁超时时间 -> get lock timeout for return
*/
private Long timeout = 300L;
/**
- LUA脚本 -> 分布式锁解锁原子操作脚本
*/
private static final String LUA_SCRIPT =
“if redis.call(‘get’,KEYS[1]) == ARGV[1] then” +
" return redis.call(‘del’,KEYS[1]) " +
“else” +
" return 0 " +
“end”;
/**
- set命令参数
*/
private SetParams params = SetParams.setParams().nx().px(millisecondsToExpire);
@Autowired
private JedisPool jedisPool;
/**
-
加锁 -> 超时锁
-
@param lockId 一个随机的不重复id -> 区分不同客户端
-
@return
*/
public boolean timeLock(String lockId) {
Jedis client = jedisPool.getResource();
long start = System.currentTimeMillis();
try {
for(;😉 {
String lock = client.set(LOCK_KEY, lockId, params);
if (“OK”.equalsIgnoreCase(lock)) {
return Boolean.TRUE;
}
// sleep -> 获取失败暂时让出CPU资源
TimeUnit.MILLISECONDS.sleep(100);
long time = System.currentTimeMillis() - start;
if (time >= timeout) {
return Boolean.FALSE;
}
}
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
} finally {
client.close();
}
return Boolean.FALSE;
}
/**
-
解锁
-
@param lockId 一个随机的不重复id -> 区分不同客户端
-
@return
*/
public boolean unlock(String lockId) {
Jedis client = jedisPool.getResource();
try {
Object result = client.eval(LUA_SCRIPT, Arrays.asList(LOCK_KEY), Arrays.asList(lockId));
if (result != null && “1”.equalsIgnoreCase(result.toString())) {
return Boolean.TRUE;
}
return Boolean.FALSE;
} catch (Exception e) {
e.printStackTrace();
logger.error(e.getMessage());
}
return Boolean.FALSE;
}
}
测试类
package com.lizba.controller;
import cn.hutool.core.util.IdUtil;
import com.lizba.utill.RedisLockUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
-
-
测试
-
@Author: Liziba
-
@Date: 2021/7/11 12:27
*/
@RestController
@RequestMapping(“/redis”)
public class TestController {
@Autowired
private RedisLockUtil redisLockUtil;
private AtomicInteger count ;
@GetMapping(“/index/{num}”)
public String index(@PathVariable int num) throws InterruptedException {
count = new AtomicInteger(0);
CountDownLatch countDownLatch = new CountDownLatch(num);
ExecutorService executorService = Executors.newFixedThreadPool(num);
Set failSet = new HashSet<>();
long start = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
executorService.execute(() -> {
long lockId = IdUtil.getSnowflake(1, 1).nextId();
try {
boolean isSuccess = redisLockUtil.timeLock(String.valueOf(lockId));
if (isSuccess) {
count.addAndGet(1);
System.out.println(Thread.currentThread().getName() + " lock success" );
} else {
failSet.add(Thread.currentThread().getName());
}
} finally {
boolean unlock = redisLockUtil.unlock(String.valueOf(lockId));
if (unlock) {
System.out.println(Thread.currentThread().getName() + " unlock success" );
}
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdownNow();
failSet.forEach(t -> System.out.println(t + " lock fail" ));
long time = System.currentTimeMillis() - start;
return String.format(“Thread sum: %d, Time sum: %d, Success sum:%d”, num, time, count.get());
}
}
测试结果
总结
虽然我个人也经常自嘲,十年之后要去成为外卖专员,但实际上依靠自身的努力,是能够减少三十五岁之后的焦虑的,毕竟好的架构师并不多。
架构师,是我们大部分技术人的职业目标,一名好的架构师来源于机遇(公司)、个人努力(吃得苦、肯钻研)、天分(真的热爱)的三者协作的结果,实践+机遇+努力才能助你成为优秀的架构师。
如果你也想成为一名好的架构师,那或许这份Java成长笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。
boolean unlock = redisLockUtil.unlock(String.valueOf(lockId));
if (unlock) {
System.out.println(Thread.currentThread().getName() + " unlock success" );
}
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdownNow();
failSet.forEach(t -> System.out.println(t + " lock fail" ));
long time = System.currentTimeMillis() - start;
return String.format(“Thread sum: %d, Time sum: %d, Success sum:%d”, num, time, count.get());
}
}
测试结果
总结
虽然我个人也经常自嘲,十年之后要去成为外卖专员,但实际上依靠自身的努力,是能够减少三十五岁之后的焦虑的,毕竟好的架构师并不多。
架构师,是我们大部分技术人的职业目标,一名好的架构师来源于机遇(公司)、个人努力(吃得苦、肯钻研)、天分(真的热爱)的三者协作的结果,实践+机遇+努力才能助你成为优秀的架构师。
如果你也想成为一名好的架构师,那或许这份Java成长笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。
[外链图片转存中…(img-8mrnrRB6-1714800495451)]