一:基于 Redis 的 NX EX
参数
既然是选用了 Redis,那么它就得具有排他性才行。同时它最好也有锁的一些基本特性:
- 高性能(加、解锁时高性能)
- 可以使用阻塞锁与非阻塞锁。
- 不能出现死锁。
- 可用性(不能出现节点 down 掉后加锁失败)。
这里利用 Redis set key
时的一个 NX 参数可以保证在这个 key 不存在的情况下写入成功。并且再加上 EX 参数可以让该 key 在超时之后自动删除。
所以利用以上两个特性可以保证在同一时刻只会有一个进程获得锁,并且不会出现死锁(最坏的情况就是超时自动删除 key)。
yml文件:
redis:
host: xxxx
password: test2
port: 6379
maxIdle: 100
maxWaitMillis: 10000
maxTotal: 600
testOnBorrow: true
testOnReturn: true
timeout: 5000
redis配置:
package com.fhgl.shop.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class RedisConfiguration {
private static Logger logger = LoggerFactory.getLogger(RedisConfiguration.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.maxWaitMillis}")
private int maxWaitMillis;
@Value("${spring.redis.maxTotal}")
private int maxTotal;
@Value("${spring.redis.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.redis.testOnReturn}")
private boolean testOnReturn;
@Value("${spring.redis.timeout}")
private int timeout;
// @Value("${spring.redis.database}")
// private int database;
@Value("${spring.redis.maxIdle}")
private int maxIdle;
// @Value("${spring.redis.pool.min-idle}")
// private int minIdle;
/**
* 连接池配置
*
* @Description:
* @return
*/
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
// jedisPoolConfig.setMinIdle(minIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setTestOnBorrow(testOnBorrow);
jedisPoolConfig.setTestOnReturn(testOnReturn);
return jedisPoolConfig;
}
/**
* redis连接的基础设置
*
* @Description:
* @return
*/
@Primary
@Bean(name = "redisConnectionFactory")
public RedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
factory.setPassword(password);
// 存储的库
factory.setDatabase(3);
// 设置连接超时时间
factory.setTimeout(timeout);
factory.setUsePool(true);
factory.setPoolConfig(jedisPoolConfig());
logger.info("redis factory3 inited finish...host:" + host + ",port:" + port);
return factory;
}
@Primary
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
setRedisTemplateSerializer(redisTemplate);
logger.info("redis restTemplate3 inited finish...");
return redisTemplate;
}
/// database 1
/**
* redis连接的基础设置1
*
* @Description:
* @return
*/
@Bean(name = "redisConnectionFactory1")
public RedisConnectionFactory redisConnectionFactory1() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
factory.setPassword(password);
// 存储的库
factory.setDatabase(1);
// 设置连接超时时间
factory.setTimeout(timeout);
factory.setUsePool(true);
factory.setPoolConfig(jedisPoolConfig());
logger.info("redis factory1 inited finish...host:" + host + ",port:" + port);
return factory;
}
@Bean(name = "redisTemplate1")
public RedisTemplate<String, Object> redisTemplate1(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory1());
setRedisTemplateSerializer(redisTemplate);
logger.info("redis restTemplate1 inited finish...");
return redisTemplate;
}
/// database 0
/**
* redis连接的基础设置0
*
* @Description:
* @return
*/
@Bean(name = "redisConnectionFactory0")
public RedisConnectionFactory redisConnectionFactory0() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
factory.setPassword(password);
// 存储的库
factory.setDatabase(0);
// 设置连接超时时间
factory.setTimeout(timeout);
factory.setUsePool(true);
factory.setPoolConfig(jedisPoolConfig());
logger.info("redis factory0 inited finish...host:" + host + ",port:" + port);
return factory;
}
@Bean(name = "redisTemplate0")
public RedisTemplate<String, Object> redisTemplate0(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory0());
setRedisTemplateSerializer(redisTemplate);
logger.info("redis restTemplate0 inited finish...");
return redisTemplate;
}
/// database 8
/**
* redis连接的基础设置8
*
* @Description:
* @return
*/
@Bean(name = "redisConnectionFactory8")
public RedisConnectionFactory redisConnectionFactory8() {
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
factory.setPassword(password);
// 存储的库
factory.setDatabase(8);
// 设置连接超时时间
factory.setTimeout(timeout);
factory.setUsePool(true);
factory.setPoolConfig(jedisPoolConfig());
logger.info("redis factory8 inited finish...host:" + host + ",port:" + port);
return factory;
}
@Bean(name = "redisTemplate8")
public RedisTemplate<String, Object> redisTemplate8(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory8());
setRedisTemplateSerializer(redisTemplate);
logger.info("redis restTemplate8 inited finish...");
return redisTemplate;
}
/**
* 设置序列化方式
*/
private void setRedisTemplateSerializer(RedisTemplate<String, Object> redisTemplate) {
StringRedisSerializer redisSerializer = new StringRedisSerializer();
// 设置Key的序列化方式
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setHashKeySerializer(redisSerializer);
// JdkSerializationRedisSerializer序列化方式;
// JdkSerializationRedisSerializer jdkRedisSerializer=new
// JdkSerializationRedisSerializer();
// redisTemplate.setValueSerializer(jdkRedisSerializer);
// redisTemplate.setHashValueSerializer(jdkRedisSerializer);
// 设置value的序列化方式
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
redisTemplate.afterPropertiesSet();
}
}
config中添加脚本配置
@Bean
public DefaultRedisScript<Long> defaultRedisScript() {
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(Long.class);
defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
// defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("delete.lua")));
return defaultRedisScript;
}
添加工具类:
package com.fhgl.user.config;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Created by hui.yunfei@qq.com on 2019/6/26
*/
public class RedisLock {
private static final Long RELEASE_SUCCESS = 1L;
//不安全
public static boolean lock(String key, String value,RedisTemplate<String, Object> template) {
//执行set命令
//Boolean absent = template.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);//1
template.setEnableTransactionSupport(true);
template.multi();
template.opsForValue().setIfAbsent(key,value);
template.expire(key,2, TimeUnit.SECONDS);
List result = template.exec(); // 这里result会返回事务内每一个操作的结果,如果setIfAbsent操作失败后,result[0]会为false。
if(true == (Boolean) result.get(0)){
// todo something...
return true;
}
return false;
}
public static boolean unlock(String key, String value,DefaultRedisScript<Long> redisScript,RedisTemplate<String, Object> template) {
//使用Lua脚本:先判断是否是自己设置的锁,再执行删除
Long result = template.execute(redisScript, Arrays.asList(key,value));
//返回最终结果
return RELEASE_SUCCESS.equals(result);
}
public static Boolean setLock(String key, String value,RedisTemplate<String, Object> template) {
SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
template.opsForValue().setIfAbsent(key, value);
template.expire(key,5, TimeUnit.SECONDS);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
};
return template.execute(sessionCallback);
}
}
setLock加锁、unLock解锁,lock方法废弃。在springboot2.0以上redis自带了设置超时的命令,2.0以下要自己控制超时。具体
参考
Test:
@Test
public void testThread(){
ExecutorService executorService= Executors.newFixedThreadPool(3);
for (int i=0;i<3;i++){
String uuid= UUID.randomUUID().toString();
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("thread in ..."+Thread.currentThread().getName());
boolean flag= RedisLock.setLock("lock:timer", uuid,redisTemplate1);
System.out.println(Thread.currentThread().getName()+"开启锁:"+flag);
if(flag){
System.out.println("没有线程在操作"+Thread.currentThread().getName()+"正常执行");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result= RedisLock.unlock("lock:timer",uuid,redisScript,redisTemplate1);
System.out.println(Thread.currentThread().getName()+"释放锁:"+result);
}else{
System.out.println("已有线程在操作"+Thread.currentThread().getName()+"退出");
}
}
});
}
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
String uuid= UUID.randomUUID().toString();
boolean flag= RedisLock.lock("lock:timer", uuid,redisTemplate1);
System.out.println("主线程获取锁:"+flag);
if(flag){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result= RedisLock.unlock("lock:timer",uuid,redisScript,redisTemplate1);
System.out.println("主线程释放锁:"+result);
}
}
result:
thread in ...pool-7-thread-1
thread in ...pool-7-thread-2
thread in ...pool-7-thread-3
pool-7-thread-3开启锁:false
已有线程在操作pool-7-thread-3退出
pool-7-thread-2开启锁:false
已有线程在操作pool-7-thread-2退出
pool-7-thread-1开启锁:true
没有线程在操作pool-7-thread-1正常执行
pool-7-thread-1释放锁:true
主线程获取锁:true
主线程释放锁:true
有一种风险就是操作线程阻塞后,锁超时自动退出,这个时候新进线程会获取到阻塞线程的锁,同样造成并发问题。这个可以把超时时间设置长一点,尽量保证操作线程正常退出。
二:redis官方推荐使用的Redisson高性能分布式锁
导入pom
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.7.0</version>
</dependency>
redissonConfig配置:
package com.fhgl.user.utils;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.maxWaitMillis}")
private int maxWaitMillis;
@Value("${spring.redis.maxTotal}")
private int maxTotal;
@Value("${spring.redis.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.redis.testOnReturn}")
private boolean testOnReturn;
@Value("${spring.redis.timeout}")
private int timeout;
// @Value("${spring.redis.database}")
// private int database;
@Value("${spring.redis.maxIdle}")
private int maxIdle;
private static final String REDIS_PROTOCOL = "redis://";
public String getHost() {
return host;
}
public String getPassword() {
return password;
}
public int getPort() {
return port;
}
public int getMaxWaitMillis() {
return maxWaitMillis;
}
public int getMaxTotal() {
return maxTotal;
}
public boolean isTestOnBorrow() {
return testOnBorrow;
}
public boolean isTestOnReturn() {
return testOnReturn;
}
public int getTimeout() {
return timeout;
}
public int getMaxIdle() {
return maxIdle;
}
@Override
public String toString() {
return "RedissonConfig [host=" + host + ", password=" + password + ", port=" + port + ", maxWaitMillis="
+ maxWaitMillis + ", maxTotal=" + maxTotal + ", testOnBorrow=" + testOnBorrow + ", testOnReturn="
+ testOnReturn + ", timeout=" + timeout + ", maxIdle=" + maxIdle + "]";
}
/**
* 获取redisson配置类
* */
public Config getConfig() {
Config config = new Config();
config.useSingleServer().setAddress(REDIS_PROTOCOL + host + ":" + port)
.setPassword(password)
.setDatabase(3)
.setConnectionMinimumIdleSize(maxIdle)
.setConnectionPoolSize(maxTotal)
.setConnectTimeout(timeout);
System.out.println("================config:" + host + "," + port);
return config;
}
}
component类:
package com.fhgl.user.utils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
@Component
public class RedissonManager {
private Config config;
private RedissonClient client;
private final static String LOCK_NAME = "RM:LOCK";
@Autowired
private RedissonConfig redissonConfig;
@PostConstruct
public void init() {
System.out.println("====================redisson init");
if(null == client) {
System.out.println("====================redisson creating");
config = redissonConfig.getConfig();
client = Redisson.create(config);
System.out.println("====================redisson create fin");
}
}
public RedissonClient getClient() {
config = redissonConfig.getConfig();
RedissonClient c = Redisson.create(config);
return c;
}
/**
* 获取锁
* @param timeout 自动解锁时间(秒),不需要则传-1
* */
public RLock getLock(String key,long timeout) {
// System.out.println("====================locking");
RLock lock = client.getLock(key);
if(-1 == timeout) {
lock.lock();
}else {
lock.lock(timeout, TimeUnit.SECONDS);
}
// System.out.println("====================locked");
return lock;
}
public boolean releaseLock(String key) {
RLock lock = client.getLock(key);
if(null != lock) {
// System.out.println("hold count:" + lock.getHoldCount());
lock.unlock();
return true;
}else {
// System.out.println("=============没有找到锁");
return false;
}
// System.out.println("=============unlock");
}
public boolean tryLock(String key){
RLock lock = client.getLock(key);
try {
return lock.tryLock(0,3,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
// System.out.println("====================locked");
}
}
getLock实现了阻塞获取锁队列。
tryLock返回Boolean类型可以根据结果来处理业务逻辑。tryLock的第一个参数是获取锁阻塞时间,就是获取不到会在这段时间一直尝试获取。第二个参数是超时时间。
Test:
@Test
public void testRedis(){
ExecutorService executorService= Executors.newFixedThreadPool(3);
for (int i=0;i<3;i++){
String uuid= UUID.randomUUID().toString();
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("thread in ..."+Thread.currentThread().getName());
boolean result=manager.tryLock("lock:timer");
System.out.println(Thread.currentThread().getName()+"开启锁:"+result);
if(result){
System.out.println("没有线程在操作"+Thread.currentThread().getName()+"正常执行");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result2=manager.releaseLock("lock:timer");
System.out.println(Thread.currentThread().getName()+"释放锁:"+result2);
}else{
System.out.println("已有线程在操作"+Thread.currentThread().getName()+"退出");
}
}
});
}
try {
Thread.sleep(3100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String uuid= UUID.randomUUID().toString();
boolean result =manager.tryLock("lock:timer");
System.out.println("主线程获取锁:"+result);
if(result){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag= manager.releaseLock("lock:timer");
System.out.println("主线程释放锁:"+flag);
}
}
result:
thread in ...pool-7-thread-1
thread in ...pool-7-thread-2
thread in ...pool-7-thread-3
pool-7-thread-1开启锁:false
已有线程在操作pool-7-thread-1退出
pool-7-thread-2开启锁:true
没有线程在操作pool-7-thread-2正常执行
pool-7-thread-3开启锁:false
已有线程在操作pool-7-thread-3退出
pool-7-thread-2释放锁:true
主线程获取锁:true
主线程释放锁:true