spring boot基于redis的LUA脚本 实现分布式锁【都是基于redis单点下】

 

一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁

1.pom.xml

org.springframework.boot

spring-boot-starter-data-redis

 

2.RedisLock 工具类 (注入spring)

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.data.redis.core.script.RedisScript;

import org.springframework.data.redis.serializer.StringRedisSerializer;

import org.springframework.stereotype.Component;

import java.util.Collections;

/**

* spring boot 1.5.X

* 使用redis 的 lua脚本 基于单点实现分布式锁

*

* lua脚本作为原子性操作,保证加锁和设置超时时间 为原子性操作

* @author sxd

* @date 2019/5/27 10:52

*/

@Component

public class RedisLock {

@Autowired

RedisTemplate redisTemplate;

private static final Long SUCCESS = 1L;

/**

* 获取锁

*

* @param lockKey redis的key

* @param value redis的value要求是随机串,防止释放其他请求的锁

* @param expireTime redis的key 的过期时间 防止死锁,导致其他请求无法正常执行业务

* @return

*/

public boolean lock(String lockKey, String value, int expireTime) {

String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then " +

" if redis.call('get',KEYS[1])==ARGV[1] then " +

" return redis.call('expire',KEYS[1],ARGV[2]) " +

" else " +

" return 0 " +

" end " +

"end";

RedisScript redisScript = new DefaultRedisScript<>(script, String.class);

//对非string类型的序列化

redisTemplate.setKeySerializer(new StringRedisSerializer());

redisTemplate.setValueSerializer(new StringRedisSerializer());

Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, String.valueOf(expireTime));

return SUCCESS.equals(result);

}

/**

* 释放锁

*

* @param lockKey redis的key

* @param value redis的value 只有value比对一致,才能确定是本请求 加的锁 才能正常释放

* @return

*/

public boolean unlock(String lockKey, String value) {

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

RedisScript redisScript = new DefaultRedisScript<>(script, String.class);

try {

Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value);

if (SUCCESS.equals(result)) {

return true;

}

} catch (Exception e) {

e.printStackTrace();

}

return false;

}

}

View Code

 

3.controller使用

/**

* 使用分布式锁 逻辑

* 1.准备好 key value expireTime

* value要求是随机字符串

* expireTime 是根据业务 衡量决定的 锁过期时间

*

* 2.获取锁

* 成功获取,则执行业务,执行完成,释放锁

* 失败获取,则重试获取,注意获取锁的时间间隔,直到获取成功,执行业务,最后释放锁

*

* 注意:

* 对于redis加锁的业务,尽量用在耗时短的业务上。

*

*/

@RequestMapping("/test")

public void test(){

boolean flag = false; //标识 是否正常获取锁

String uuid = UUID.randomUUID().toString(); //redis的value 是一串随机数

flag = lock.lock("mykey1",uuid,5);

if (flag){

business(uuid);

}else {

//如果未正常获取锁 可以通过重试 直到获取锁成功

while (!flag){

try {

//重试 时间间隔 减少与redis交互次数

Thread.sleep(3000);

System.out.println("重试");

flag = lock.lock("mykey1",uuid,5);

if (flag){

business(uuid);

}else {

continue;

}

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

public void business(String uuid){

try {

System.out.println("加锁成功,执行业务");

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

}finally {

//业务执行完成 正常释放锁

lock.unlock("mykey1",uuid);

}

}

View Code

 

 

 

二.spring boot 2.x 基于redis 的LUA脚本 实现分布式锁

1.pom.xml

org.springframework.boot

spring-boot-starter-data-redis

org.apache.commons

commons-pool2

2.4.2

com.fasterxml.jackson.core

jackson-core

com.fasterxml.jackson.core

jackson-annotations

com.fasterxml.jackson.core

jackson-databind

 

 

2.替代SpringBoot自动配置的RedisTemplate的RedisConfig类

 

package com.sxd.swapping.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;

import com.fasterxml.jackson.annotation.PropertyAccessor;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;

import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;

/**

* @author sxd

* @date 2019/5/27 16:13

*/

/**

* @Description Redis配置类,替代SpringBoot自动配置的RedisTemplate,参加RedisAutoConfiguration

*/

@Configuration

@AutoConfigureAfter(RedisAutoConfiguration.class)

public class RedisConfig {

@Bean

public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {

RedisTemplate template = new RedisTemplate<>();

template.setConnectionFactory(redisConnectionFactory);

//Jackson序列化器

Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

ObjectMapper om = new ObjectMapper();

om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

jackson2JsonRedisSerializer.setObjectMapper(om);

//字符串序列化器

StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

//普通Key设置为字符串序列化器

template.setKeySerializer(stringRedisSerializer);

//Hash结构的key设置为字符串序列化器

template.setHashKeySerializer(stringRedisSerializer);

//普通值和hash的值都设置为jackson序列化器

template.setValueSerializer(jackson2JsonRedisSerializer);

template.setHashValueSerializer(jackson2JsonRedisSerializer);

template.afterPropertiesSet();

return template;

}

}

View Code

 

3.RedisLock工具类,自动注入Spring

package com.sxd.swapping.utils;

import org.apache.log4j.Logger;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.stereotype.Component;

import java.util.Arrays;

import java.util.List;

/**

*

* spring boot 2.x版本

* @author sxd

* @date 2019/5/27 16:11

*/

@Component

public class RedisLock2 {

Logger logger = Logger.getRootLogger();

static final Long LOCK_SUCCESS = 1L;

static final Long LOCK_EXPIRED = -1L;

@Autowired

RedisTemplate redisTemplate;

//定义获取锁的lua脚本

private final static DefaultRedisScript LOCK_LUA_SCRIPT = new DefaultRedisScript<>(

"if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end"

, Long.class

);

//定义释放锁的lua脚本

private final static DefaultRedisScript UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>(

"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end"

, Long.class

);

/**

* 加锁

* @param key redis键值对 的 key

* @param value redis键值对 的 value 随机串作为值

* @param timeout redis键值对 的 过期时间 pexpire 以毫秒为单位

* @param retryTimes 重试次数 即加锁失败之后的重试次数,根据业务设置大小

* @return

*/

public boolean lock(String key,String value ,long timeout, int retryTimes) {

try {

logger.debug("加锁信息:lock :::: redisKey = " + key + " requestid = " + value);

//组装lua脚本参数

List keys = Arrays.asList(key);

//执行脚本

Object result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys,value,timeout);

//存储本地变量

if(LOCK_SUCCESS.equals(result)) {

logger.info("成功加锁:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);

return true;

} else if (retryTimes == 0) {

//重试次数为0直接返回失败

return false;

} else {

//重试获取锁

logger.info("重试加锁:retry to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);

int count = 0;

while(true) {

try {

//休眠一定时间后再获取锁,这里时间可以通过外部设置

Thread.sleep(100);

result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys);

if(LOCK_SUCCESS.equals(result)) {

logger.info("成功加锁:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);

return true;

} else {

count++;

if (retryTimes == count) {

logger.info("加锁失败:fail to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result);

return false;

} else {

logger.warn(count + " times try to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result);

continue;

}

}

} catch (Exception e) {

logger.error("加锁异常:acquire redis occured an exception:" + Thread.currentThread().getName(), e);

break;

}

}

}

} catch (Exception e1) {

logger.error("加锁异常:acquire redis occured an exception:" + Thread.currentThread().getName(), e1);

}

return false;

}

/**

* 释放KEY

* @param key 释放本请求对应的锁的key

* @param value 释放本请求对应的锁的value 是不重复随即串 用于比较,以免释放别的线程的锁

* @return

*/

public boolean unlock(String key,String value) {

try {

//组装lua脚本参数

List keys = Arrays.asList(key);

logger.debug("解锁信息:unlock :::: redisKey = " + key + " requestid = " + value);

// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁

Object result = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys, value);

//如果这里抛异常,后续锁无法释放

if (LOCK_SUCCESS.equals(result)) {

logger.info("解锁成功:release lock success:" + Thread.currentThread().getName() + ", Status code reply=" + result);

return true;

} else if (LOCK_EXPIRED.equals(result)) {

//返回-1说明获取到的KEY值与requestId不一致或者KEY不存在,可能已经过期或被其他线程加锁

// 一般发生在key的过期时间短于业务处理时间,属于正常可接受情况

logger.warn("解锁异常:release lock exception:" + Thread.currentThread().getName() + ", key has expired or released. Status code reply=" + result);

} else {

//其他情况,一般是删除KEY失败,返回0

logger.error("解锁失败:release lock failed:" + Thread.currentThread().getName() + ", del key failed. Status code reply=" + result);

}

} catch (Exception e) {

logger.error("解锁异常:release lock occured an exception", e);

}

return false;

}

}

View Code

 

 

4.使用

@Autowired

RedisLock2 lock2;

@Autowired

RedisTemplate redisTemplate;

@RequestMapping("/test3")

public void test3(){

ValueOperations vops = redisTemplate.opsForValue();

String uuid = UUID.randomUUID().toString();

//加锁

if (lock2.lock("mykey1",uuid,5000,3)){

try {

// 执行业务

System.out.println("加锁成功,做业务");

vops.increment(REDIS_COUNT_KEY,1);

Thread.sleep(3000);

System.out.println("业务执行结束");

} catch (InterruptedException e) {

e.printStackTrace();

}finally {

//解锁

lock2.unlock("mykey1",uuid);

}

}

}

View Code

 

查看原文

发表评论

返回顶部暗黑模式