一:基於 Redis 的 NX EX
參數
既然是選用了 Redis,那麼它就得具有排他性才行。同時它最好也有鎖的一些基本特性:
- 高性能(加、解鎖時高性能)
- 可以使用阻塞鎖與非阻塞鎖。
- 不能出現死鎖。
- 可用性(不能出現節點 down 掉後加鎖失敗)。
這裏利用 Redis set key
時的一個 NX 參數可以保證在這個 key 不存在的情況下寫入成功。並且再加上 EX 參數可以讓該 key 在超時之後自動刪除。
所以利用以上兩個特性可以保證在同一時刻只會有一個進程獲得鎖,並且不會出現死鎖(最壞的情況就是超時自動刪除 key)。
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 [email protected] 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以下要自己控制超時。具體
參考https://segmentfault.com/a/1190000017933697?utm_source=tag-newest
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>
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