This article will introduce how to use Redis to implement a safe and reliable distributed lock, and explain the main elements and common misunderstandings of distributed lock implementation. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to everyone.
# In a concurrent scenario, when multiple processes or threads share resources for reading and writing, access to resources needs to be guaranteed to be mutually exclusive. In a stand-alone system, we can use the API in the Java concurrency package, the synchronized keyword, etc. to solve the problem; but in a distributed system, these methods are no longer applicable, and we need to implement distributed locks ourselves.
Common distributed lock implementation solutions include: database-based, Redis-based, Zookeeper-based, etc. As part of the Redis topic, this article will talk about the implementation of distributed locks based on Redis. [Related recommendations: Redis Video Tutorial]
Distributed locks and JVM built-in locks have the same purpose: to allow applications to access or operate shared resources in the expected order, and to prevent multiple threads from operating the same resource at the same time, causing the system to run disorderly and uncontrollably. Often used in scenarios such as product inventory deductions and coupon deductions.
Theoretically, in order to ensure the security and effectiveness of the lock, the distributed lock needs to meet at least the following conditions:
In terms of implementation, distributed locks are roughly divided into three steps:
Whether it is Java's built-in lock or distributed lock, it does not matter Which distributed implementation solution is used depends on the two steps a and c. Redis is naturally friendly for implementing distributed locks for the following reasons:
SET key value NX PX milliseconds
command adds a key with expiration time when the key does not exist to provide support for security locking. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${your-spring-boot-version}</version> </dependency>
Add the following content to application.properties, stand-alone Redis instance.
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"); } }
Maybe many students (including me) use the above implementation method in their daily work, which seems to be safe:
set
Command NX
, PX
options to lock, ensuring mutual exclusion of locks and avoiding deadlock; In fact, there is a prerequisite for the above implementation to be stable: stand-alone version of Redis, enable AOF persistence mode and set appendfsync=always
.
But there may be problems in sentinel mode and cluster mode. Why?
Sentinel mode and cluster mode are based on the master-slave architecture. Data synchronization is achieved between the master and the slave through command propagation, and the command propagation is asynchronous.
So there is a possibility that the master node data is written successfully, but the master node goes down before the slave node is notified.
When the slave node is promoted to the new master node through failover, other threads have the opportunity to re-lock successfully, resulting in the mutual exclusion condition of the distributed lock being not met.
In cluster mode, if all nodes in the cluster run stably and failover does not occur, security is guaranteed. However, no system can guarantee 100% stability, and distributed locks based on Redis must consider fault tolerance.
Since master-slave synchronization is based on the principle of asynchronous replication, sentinel mode and cluster mode are inherently unable to meet this condition. For this reason, the Redis author specially proposed a solution-RedLock (Redis Distribute Lock).
According to the official document, the design idea of RedLock is introduced.
Let’s talk about the environment requirements first. N (N>=3) independently deployed Redis instances are required. Master-slave replication, failover and other technologies are not required between each other.
In order to obtain the lock, the client will follow the following process:
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的思想,我们是不是也可以在自己的应用程序内实现了分布式锁呢?欢迎沟通!
更多编程相关知识,请访问:编程入门!!
The above is the detailed content of Use Redis to implement a safe and reliable distributed lock. For more information, please follow other related articles on the PHP Chinese website!