集羣情況下,JDK的鎖是很容易出現問題的,這時候就需要用到分佈式鎖;最近用到了Redis實現分佈式鎖,這裏記錄一下。
基本原理:
這裏使用了Redis的setNX,由於當某個 key 不存在的時候,SETNX 纔會設置該 key。且由於 Redis 採用單進程單線程模型,所以,不需要擔心併發的問題。那麼,就可以利用 SETNX 的特性維護一個 key,存在的時候,即鎖被某個線程持有;不存在的時候,沒有線程持有鎖。
該實例使用jMeter工具進行過壓測,可以抗住每秒幾百的併發。
1. pom文件添加依賴
<!-- redis依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2. Redis配置
# Redis數據庫索引(默認爲0)
spring.redis.database=0
# Redis服務器地址
spring.redis.host=127.0.0.1
# Redis服務器連接端口
spring.redis.port=6379
# Redis服務器連接密碼(默認爲空)
spring.redis.password=
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.pool.max-active=200
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.pool.max-wait=-1
# 連接池中的最大空閒連接
spring.redis.pool.max-idle=10
# 連接池中的最小空閒連接
spring.redis.pool.min-idle=1
# 連接超時時間(毫秒)
spring.redis.timeout=10000
#是否在從池中取出連接前進行檢驗,如果檢驗失敗,則從池中去除連接並嘗試取出另一個
redis.testOnBorrow=true
#在空閒時檢查有效性, 默認false
redis.testWhileIdle=true
3. redis 分佈式鎖代碼實現
package com.example.demo.utils;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
/**
* redis鎖工具類
*/
@Component
public class RedisLockUtil {
private final static Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);
@Autowired
public StringRedisTemplate redisTemplate;
/** 執行結果標識 */
private static final Long SUCCESS = 1L;
/** 設置成功標識 */
private static final String LOCK_SUCCESS = "OK";
/** key不存在時才設置值 */
private static final String SET_IF_NOT_EXIST = "NX";
/** 過期時間單位標識,EX:秒 */
private static final String SET_WITH_EXPIRE_TIME = "EX";
/**
* 鎖的過期時間(單位:秒)
*/
private static final int LOCK_EXPIRE_TIME = 30;
/**
* 最大嘗試次數
*/
private static final int MAX_ATTEMPTS = 100;
/**
* key前綴
*/
private static final String KEY_PRE = "REDIS_LOCK_";
/**
* 獲取鎖
* @param key
* @return
*/
public Boolean getLock(String key, String value) {
return tryLock(key, value,MAX_ATTEMPTS);
}
/**
* 嘗試加鎖
* @param key
* @param max_attempts 重試次數
* @return
*/
public Boolean tryLock(String key, String value, Integer max_attempts) {
int attempt_counter = 0;
key = KEY_PRE + key;
while (attempt_counter < max_attempts) {
if (setLock(key, value, LOCK_EXPIRE_TIME)) {
System.out.println("獲取鎖成功,Redis Lock key : " + key + ", value : " + value);
return Boolean.TRUE;
}
// System.out.println("正在重試獲取鎖,Redis Lock key : " + key + ", value : " + value);
attempt_counter++;
if (attempt_counter >= max_attempts) {
logger.error("獲取鎖失敗!, " + key);
}
try {
// 休眠時間 10-100 毫秒
Thread.sleep((int)(Math.random() * 90 + 10));
} catch (InterruptedException e) {
logger.error("獲取鎖等待異常:{}",e.getMessage());
}
}
return Boolean.FALSE;
}
/**
* 加鎖 key不存在時才設置key value
* @param lockKey 加鎖鍵
* @param value 加鎖客戶端唯一標識
* @param seconds 鎖過期時間
* @return
*/
public Boolean setLock(String lockKey, String value, long seconds) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(lockKey, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 解鎖
* @param key 加鎖鍵
* @param value
* @return
*/
public boolean unLock(String key, String value) {
key = KEY_PRE + key;
try {
String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
List<String> keys = Arrays.asList(key, value);
Object result = redisTemplate.execute(redisScript, keys);
if(SUCCESS.equals(result)) {
System.out.println("釋放Redis鎖 : " + key + ", value : " + value);
return true;
}
} catch (Exception e) {
logger.error("釋放Redis鎖異常:",e);
}
return false;
}
/**
* 生成加鎖的唯一字符串
* @return 唯一字符串
*/
public String fetchLockValue() {
return UUID.randomUUID().toString() + "-" + System.currentTimeMillis();
}
}
4. 使用實例
/**
* 使用實例
* @return
*/
@Transactional(rollbackFor=Exception.class)
public String updateInfoLock(){
// 鎖key
String key = "updateInfo";
// 鎖的唯一字符串
String value = redisLockUtil.fetchLockValue();
// 獲取鎖
if (!redisLockUtil.getLock(key,value)) {
log.info("沒搶到鎖!");
return "服務器繁忙,請稍後重試!";
}
try {
// 業務邏輯代碼
} catch (Exception e) {
log.error("XXX異常",e);
// 捕獲異常之後需要 手動回滾事務
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}finally{
// 釋放鎖
redisLockUtil.unLock(key, value);
}
return "操作成功!";
}
注意:釋放鎖的執行代碼最好是放在 finally 代碼塊裏面,防止出現異常或者忘記釋放鎖的情況;