Home  >  Article  >  Database  >  Use Redis to implement a safe and reliable distributed lock

Use Redis to implement a safe and reliable distributed lock

青灯夜游
青灯夜游forward
2021-04-19 10:20:322029browse

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.

Use Redis to implement a safe and reliable distributed lock

# 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]

Analysis and Implementation


Problem Analysis

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:

  • Mutual exclusivity: At the same time, there is only one The thread can acquire the lock;
  • No deadlock: After the thread acquires the lock, it must be guaranteed to be released. Even if the application goes down after the thread acquires the lock, it can be released within a limited time;
  • Add Locking and unlocking must be in the same thread;

In terms of implementation, distributed locks are roughly divided into three steps:

  • a-Obtain the right to operate the resource;
  • b-Perform operations on resources;
  • c-Release the operation rights of resources;

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:

  • Redis uses single-thread processing during the command processing phase. Only one thread can process the same key at the same time, so there is no multi-thread race condition problem.
  • 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.
  • Lua scripts and DEL commands provide reliable support for safe unlocking.

Code implementation

  • Maven dependency
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  	<version>${your-spring-boot-version}</version>
</dependency>
  • Configuration file

Add the following content to application.properties, stand-alone Redis instance.

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
  • RedisConfig
@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;
    }
}
  • RedisLock
@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(&#39;get&#39;,KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;,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;
    }
}
  • Test Case
@SpringBootTest
class RedisDistLockDemoApplicationTests {

    @Resource
    private RedisLock redisLock;

    @Test
    public void testLock() {
        redisLock.tryAcquire("abcd", "abcd", 5 * 60 * 1000, 5 * 1000);
        redisLock.releaseLock("abcd", "abcd");
    }
}

Safety risks

Maybe many students (including me) use the above implementation method in their daily work, which seems to be safe:

  • Use setCommand NX, PX options to lock, ensuring mutual exclusion of locks and avoiding deadlock;
  • Use lua script to unlock , to prevent unlocking other threads;
  • Lock and unlock commands are all atomic operations;

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.

Official RedLock


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).

Design Idea

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:

  • 获取当前时间(毫秒)作为开始时间start;
  • 使用相同的key和随机value,按顺序向所有N个节点发起获取锁的请求。当向每个实例设置锁时,客户端会使用一个过期时间(小于锁的自动释放时间)。比如锁的自动释放时间是10秒,这个超时时间应该是5-50毫秒。这是为了防止客户端在一个已经宕机的实例浪费太多时间:如果Redis实例宕机,客户端尽快处理下一个实例。
  • 客户端计算加锁消耗的时间cost(cost=start-now)。只有客户端在半数以上实例加锁成功,并且整个耗时小于整个有效时间(ttl),才能认为当前客户端加锁成功。
  • 如果客户端加锁成功,那么整个锁的真正有效时间应该是:validTime=ttl-cost。
  • 如果客户端加锁失败(可能是获取锁成功实例数未过半,也可能是耗时超过ttl),那么客户端应该向所有实例尝试解锁(即使刚刚客户端认为加锁失败)。

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!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete