使用Redis作為分佈式鎖是非常常見的。相對於基於數據庫的鎖,Redis分佈式鎖的效率更高、更可靠、更方便。在使用Redis時,RedisTemplate是一個很好的工具類。那麼本文就來詳細介紹RedisTemplate分佈式鎖。
一、使用Jedis實現RedisTemplate
RedisTemplate是Spring Data Redis提供的一個在Java環境下訪問Redis數據庫的接口。在使用RedisTemplate分佈式鎖時,首先要實例化RedisTemplate,並將JedisConnectionFactory作為它的構造參數。
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
private RedisTemplate redisTemplate;
private String lockKey;
private String threadId;
private long keepMills;
private boolean locked = false;
public RedisLock(RedisTemplate redisTemplate, String lockKey, long keepMills) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.keepMills = keepMills;
this.threadId = String.valueOf(Thread.currentThread().getId());
}
//... more code ...
}
這裡要注意配置文件:在實例化JedisConnectionFactory時,需要配置Redis的IP地址、端口和密碼等信息。以下是示例代碼:
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.password}")
private String password;
@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.max-wait}")
private long maxWait;
@Value("${spring.redis.timeout}")
private long timeout;
@Bean
JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setDatabase(database);
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
JedisClientConfiguration.JedisPoolingClientConfigurationBuilder poolingBuilder = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder) JedisClientConfiguration.builder();
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWait);
poolingBuilder.poolConfig(jedisPoolConfig);
JedisClientConfiguration jedisClientConfiguration = poolingBuilder.build();
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration);
return jedisConnectionFactory;
}
@Bean(name = "redisTemplate")
public RedisTemplate redisTemplate() {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(jedisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
二、實現簡單的Redis分佈式鎖
首先,RedisTemplate分佈式鎖需要使用SETNX命令,如果返回值為1,表示加鎖成功,如果為0,說明鎖已經被其他線程持有。
下面是加鎖操作的代碼:
public boolean lock() {
long now = System.currentTimeMillis();
long expired = now + keepMills + 1;
String expiredStr = String.valueOf(expired);
if (redisTemplate.opsForValue().setIfAbsent(lockKey, expiredStr)) {
locked = true;
return true;
}
String currentValueStr = (String) redisTemplate.opsForValue().get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < now) {
// lock is expired
String oldValueStr = (String) redisTemplate.opsForValue().getAndSet(lockKey, expiredStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
locked = true;
return true;
}
}
return false;
}
代碼中的keepMills表示鎖的過期時間,單位是毫秒。代碼邏輯是:首先嘗試加鎖;若獲取鎖成功,則設置locked為true,返回true;若不能獲取鎖,則檢查鎖是否過期,如果過期,使用getset方法獲取舊值並將新值設置到鎖的key中。如果新舊值相同,則成功獲取鎖,否則繼續競爭鎖。
接下來是解鎖操作的代碼:
public void unlock() {
if (locked) {
redisTemplate.delete(lockKey);
locked = false;
}
}
解鎖操作比較簡單,只需要將key刪除即可。
三、優化Redis分佈式鎖
上面的Redis分佈式鎖實現雖然可以滿足基本的使用,但仍存在一些問題,如高並發時鎖可能會失效、鎖的時間過長等。下面分別介紹優化方案。
1.使用Lua腳本提高並發性
為了避免高並發時鎖的失效,可以使用Lua腳本來確保Redis分佈式鎖的原子性。
Lua腳本要比Java代碼快,並且可以確保Redis分佈式鎖的原子性。以下是使用Lua腳本實現分佈式鎖的代碼:
private static final String LOCK_SCRIPT =
"if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 " +
"then " +
" redis.call('PEXPIRE', KEYS[1], ARGV[2]); " +
" return true; " +
"else " +
" return false; " +
"end; ";
private static final String UNLOCK_SCRIPT =
"if redis.call('GET', KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call('DEL', KEYS[1]); " +
"else " +
" return 0; " +
"end; ";
public boolean lock() {
long now = System.currentTimeMillis();
long expired = now + keepMills + 1;
String expiredStr = String.valueOf(expired);
RedisScript redisScript = new DefaultRedisScript(LOCK_SCRIPT, Boolean.class);
boolean success = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), expiredStr, String.valueOf(keepMills));
locked = success;
return success;
}
public void unlock() {
RedisScript redisScript = new DefaultRedisScript(UNLOCK_SCRIPT, Long.class);
redisTemplate.execute(redisScript, Collections.singletonList(lockKey), String.valueOf(System.currentTimeMillis()));
locked = false;
}
代碼中,LOCK_SCRIPT和UNLOCK_SCRIPT是Lua腳本,分別用於加鎖和解鎖操作。使用RedisTemplate執行腳本時,將腳本和參數傳遞給execute方法即可。
2.使用Redisson優化鎖的時間
如果在分佈式系統中鎖的時間過長,可能會導致死鎖的問題。為了避免這種情況,可以使用Redisson提供的可重入鎖。
Redisson是一個Java Redis客戶端,實現了分佈式和可擴展的Java數據結構。Redisson的可重入鎖將使用累計次數來遞增釋放鎖的次數。這意味着任何線程都可以釋放鎖,而不僅僅是那個持有鎖的線程。
public boolean lock() {
RLock redissonLock = redissonClient.getLock(lockKey);
try {
boolean success = redissonLock.tryLock(0, keepMills, TimeUnit.MILLISECONDS);
locked = success;
return success;
} catch (InterruptedException e) {
return false;
}
}
public void unlock() {
if (locked) {
RLock redissonLock = redissonClient.getLock(lockKey);
redissonLock.unlock();
}
}
代碼中的RLock表示Redisson的可重入鎖。在加鎖時,調用tryLock方法,如果獲取鎖成功,則設置locked為true,返回true。在解鎖時,直接調用unlock方法即可。
四、總結
本文詳細介紹了RedisTemplate分佈式鎖的實現原理和優化方式。使用RedisTemplate分佈式鎖可以幫助我們解決在分佈式環境下數據一致性的問題,是分佈式系統中比較常見和可靠的鎖實現方式。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/304242.html