一、使用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分布式锁可以帮助我们解决在分布式环境下数据一致性的问题,是分布式系统中比较常见和可靠的锁实现方式。