本篇文章给大家介绍一下如何使用redis实现一个安全可靠的分布式锁,说明分布式锁实现的主要要素,常见误区。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。

并发场景下多个进程或线程共享资源的读写,需要保证对资源的访问互斥。在单机系统中,我们可以使用Java并发包中的API、synchronized关键字等方式来解决;但是在分布式系统下,这些方式不再适用,我们需要自己实现分布式锁。
常见的分布式锁的实现方案有:基于数据库、基于Redis、基于Zookeeper等。作为Redis专题的一部分,本文将基于Redis聊一聊分布式锁的实现方案。【相关推荐:Redis视频教程】
分布式锁与JVM内置的锁有着共同的目的:让应用程序以预期的顺序访问或操作共享的资源,防止多个线程同时对同一资源操作,导致系统运行紊乱、不可控。常常用于商品库存扣减、优惠券扣减等场景。
理论上来讲,为了保证锁的安全性和有效性,分布式锁至少需要满足以下条件:
在实现方式上,分布式锁大体分为三个步骤:
无论是Java内置的锁,还是分布式锁,也无论使用哪种分布式实现方案,都是围绕a、c两个步骤展开。Redis对于实现分布式锁天然友好,原因如下:
SET key value NX PX milliseconds命令在不存在key的情况下添加具有过期时间的key,为安全加锁提供支持。<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${your-spring-boot-version}</version>
</dependency>在application.properties增加以下内容,单机版Redis实例。
spring.redis.database=0 spring.redis.host=localhost spring.redis.port=6379
@Configuration
public class RedisConfig {
// 自己定义了一个 RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory)
throws UnknownHostException {
// 我们为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String,
Object>();
template.setConnectionFactory(factory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}@Service
public class RedisLock {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 加锁,最多等待maxWait毫秒
*
* @param lockKey 锁定key
* @param lockValue 锁定value
* @param timeout 锁定时长(毫秒)
* @param maxWait 加锁等待时间(毫秒)
* @return true-成功,false-失败
*/
public boolean tryAcquire(String lockKey, String lockValue, int timeout, long maxWait) {
long start = System.currentTimeMillis();
while (true) {
// 尝试加锁
Boolean ret = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, timeout, TimeUnit.MILLISECONDS);
if (!ObjectUtils.isEmpty(ret) && ret) {
return true;
}
// 计算已经等待的时间
long now = System.currentTimeMillis();
if (now - start > maxWait) {
return false;
}
try {
Thread.sleep(200);
} catch (Exception ex) {
return false;
}
}
}
/**
* 释放锁
*
* @param lockKey 锁定key
* @param lockValue 锁定value
* @return true-成功,false-失败
*/
public boolean releaseLock(String lockKey, String lockValue) {
// lua脚本
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.opsForValue().getOperations().execute(redisScript, Collections.singletonList(lockKey), lockValue);
return result != null && result > 0L;
}
}@SpringBootTest
class RedisDistLockDemoApplicationTests {
@Resource
private RedisLock redisLock;
@Test
public void testLock() {
redisLock.tryAcquire("abcd", "abcd", 5 * 60 * 1000, 5 * 1000);
redisLock.releaseLock("abcd", "abcd");
}
}可能很多同学(也包括我)在日常工作中都是使用上面的实现方式,看似是稳妥的:
set命令NX、PX选项进行加锁,保证了加锁互斥,避免了死锁;其实以上实现的稳妥有个前提条件:单机版Redis、开启AOF持久化方式并设置appendfsync=always。
但是在哨兵模式和集群模式下可能存在问题,为什么呢?
哨兵模式和集群模式基于主从架构,主从之间通过命令传播实现数据同步,而命令传播是异步的。
所以就存在主节点数据写入成功,在还未通知从节点情况下,主节点就宕机的可能。
当从节点通过故障转移提升为新的主节点后,其他线程就有机会重新加锁成功,导致不满足分布式锁的互斥条件。
集群模式下,若集群所有节点稳定运行,不出现故障转移的情况下,安全性是有保障的。但是,没有什么系统能够保证100%稳定,基于Redis的分布式锁必须考虑容错。
由于主从同步基于异步复制原理,所以哨兵模式和集群模式天生无法满足此条件。为此,Redis作者专门提出了一种解决方案——RedLock(Redis Distribute Lock)。
根据官方文档的说明,把RedLock的设计思路进行介绍。
先说环境要求,需要N(N>=3)个独立部署的Redis实例,相互之间不需要主从复制、故障转移等技术。
为了获取锁,客户端将按照以下流程进行操作:
RedLock的设计思路延续了Redis内部多种场景的投票方案,通过多个实例分别加锁解决竞态问题,虽然加锁消耗了时间,但是消除了主从机制下的安全问题。
官方推荐Java实现为Redisson,它具备可重入特性,按照RedLock进行实现,支持独立实例模式、集群模式、主从模式、哨兵模式等;API比较简单,上手容易。示例如下(直接通过测试用例):
@Test
public void testRedLock() throws InterruptedException {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
final RedissonClient client = Redisson.create(config);
// 获取锁实例
final RLock lock = client.getLock("test-lock");
// 加锁
lock.lock(60 * 1000, TimeUnit.MILLISECONDS);
try {
// 假装做些什么事情
Thread.sleep(50 * 1000);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//解锁
lock.unlock();
}
}Redisson封装的非常好,我们可以像使用Java内置的锁一样去使用,代码简洁的不能再少了。关于Redisson源码的分析,网上有很多文章大家可以找找看。
分布式锁是我们研发过程中常用的的一种解决并发问题的方式,Redis是只是一种实现方式。
关键的是要弄清楚加锁、解锁背后的原理,以及实现分布式锁需要解决的核心问题,同时考虑我们所采用的中间件有什么特性可以支撑。了解这些后,实现起来就不是什么问题了。
学习了RedLock的思想,我们是不是也可以在自己的应用程序内实现了分布式锁呢?欢迎沟通!
更多编程相关知识,请访问:编程入门!!
以上就是使用Redis实现一个安全可靠的分布式锁的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号