上一篇文章讲的是 redis + lua实现 分布式限流,这篇文章是在上篇文章的项目结构添加了 分布锁的相关代码,如果碰到说个别的pom或者配置没有贴出来,请查看我的上篇文章 :https://blog.csdn.net/weixin_38003389/article/details/89049135
本文介绍的是利用 redis 实现分布式锁,redis单机操作。可能很多人看到这篇文章之前也会看其他兄台写的。分布式锁无非就两个操作,第一步“上锁”,第二步“解锁”,网上案例在上锁的操作上会有很大区别,本文在对上锁的操作采用一步到位,保证上锁操作的原子性,我觉得总比两步操作的姿势要舒服的多吧。
介绍一下本次使用所有框架和中间件的版本
框架 | 版本 |
Spring Boot | 2.0.3.RELEASE |
Spring Cloud | Finchley.RELEASE |
redis | redis-4.0.11 |
JDK | 1.8.x |
这里的 父pom 除上文已有的依赖之外,只需要再添加如下即可
redis.clients
jedis
首先我们创建一个本次核心的工程,这个工程完全可以是你们项目里公共工程的其中一个文件夹,但在我的 Demo 中,我这个核心的工程起名叫 redis-tool。
我们准备一个 lua 脚本,把以下代码复制 ,粘贴到 redis-tool 项目中的 resources 目录下,起名 lock.lua 即可。
解锁 lua 脚本
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
脚本解释:
这个脚本是解锁时使用的,参数KEYS[1]赋值为第一个参数,是要解锁的 key,ARGV[1]赋值为requestId。
Redis配置类
该配置类叫 RedisConfig ,继上文(https://blog.csdn.net/weixin_38003389/article/details/89049135)进行改造。
@Configuration
public class RedisConfig {
private Logger logger = LoggerFactory.getLogger(RedisConfig.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
/**
* 创建一个redisPool对象
*
* @return
*/
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);
logger.info("JedisPool注入成功!");
logger.info("redis地址:" + host + ":" + port);
return jedisPool;
}
/**
* 读取限流lua脚本
*
* @return
*/
@Bean
public DefaultRedisScript redisluaScript() {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
redisScript.setResultType(Number.class);
return redisScript;
}
/**
* 读取解锁lua脚本
*
* @return
*/
@Bean
public DefaultRedisScript redisLockLuaScript() {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
redisScript.setResultType(Number.class);
return redisScript;
}
/**
* redis序列化
* @param connectionFactory
* @return
*/
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
@Component
public class RedisLock {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
@Autowired
private JedisPool jedisPool;
@Autowired
private DefaultRedisScript redisLockLuaScript;
/**
* 尝试获取分布式锁
*
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
Jedis jedis = jedisPool.getResource();
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
代码解释:
我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()
方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。
小结:执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
一步加锁操作可以保证要么加锁成功,要么加锁失败。
/**
* 释放分布式锁
*
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public boolean releaseDistributedLock(String lockKey, String requestId) {
Jedis jedis = jedisPool.getResource();
Object result = jedis.eval(redisLockLuaScript.getScriptAsString(), Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
大家对解锁操作因该没什么疑问,就是执行一个 脚本而已。
测试
伪集群的方式测试多个请求同时 加锁和解锁,创建一个 eureka 的客户端,在main 方法中操作,代码如下:
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(value = {"com.annotaion", "cn.springcloud", "com.config", "com.redislock"})
public class Ch34EurekaClientApplication implements ApplicationRunner {
private static final ExecutorService executorService = Executors.newFixedThreadPool(5);
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
private static final String uuid = UUID.randomUUID().toString();
@Autowired
RedisLock redisLock;
public static void main(String[] args) {
SpringApplication.run(Ch34EurekaClientApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
for (int i = 0; i < 5; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "开始等待其他线程");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "线程就位,即将同时执行");
boolean result = redisLock.tryGetDistributedLock("lock", uuid, 1000);
if (result) {
System.out.println(Thread.currentThread().getName() + "获取成功,并开始执行业务逻辑");
result = redisLock.releaseDistributedLock("lock", uuid);
if (result) {
System.out.println(Thread.currentThread().getName() + "释放成功");
}
} else {
System.out.println(Thread.currentThread().getName() + "获取失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
});
}
executorService.shutdown();
}
}
以下是必要的控制台日志输出
总结:
以上就是 单机 redis 实现分布式锁的正确姿势,如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson
实现分布式锁。
参考文章:http://www.cnblogs.com/linjiqin/p/8003838.html
https://juejin.im/post/5ba0a098f265da0adb30c684
本文工程结构: