Jadual Kandungan
分析与实现
代码实现
安全隐患
官方RedLock
设计思路
全文总结
Rumah pangkalan data Redis 使用Redis实现一个安全可靠的分布式锁

使用Redis实现一个安全可靠的分布式锁

Apr 19, 2021 am 10:20 AM
redis Kunci yang diedarkan

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

使用Redis实现一个安全可靠的分布式锁

并发场景下多个进程或线程共享资源的读写,需要保证对资源的访问互斥。在单机系统中,我们可以使用Java并发包中的API、synchronized关键字等方式来解决;但是在分布式系统下,这些方式不再适用,我们需要自己实现分布式锁。

常见的分布式锁的实现方案有:基于数据库、基于Redis、基于Zookeeper等。作为Redis专题的一部分,本文将基于Redis聊一聊分布式锁的实现方案。【相关推荐:Redis视频教程

分析与实现


问题分析

分布式锁与JVM内置的锁有着共同的目的:让应用程序以预期的顺序访问或操作共享的资源,防止多个线程同时对同一资源操作,导致系统运行紊乱、不可控。常常用于商品库存扣减、优惠券扣减等场景。

理论上来讲,为了保证锁的安全性和有效性,分布式锁至少需要满足以下条件:

  • 互斥性:在同一时间内,仅有一个线程能够获得锁;
  • 无死锁:线程获取锁后,必须保证能够释放,即使线程获取锁后应用程序宕机,也能在限定时间内释放;
  • 加锁和解锁必须是同一个线程;

在实现方式上,分布式锁大体分为三个步骤:

  • a-获取资源的操作权;
  • b-对资源执行操作;
  • c-释放资源的操作权;

无论是Java内置的锁,还是分布式锁,也无论使用哪种分布式实现方案,都是围绕a、c两个步骤展开。Redis对于实现分布式锁天然友好,原因如下:

  • 命令处理阶段Redis使用单线程处理,同一个key同时只有一个线程能够处理,没有多线程竞态问题。
  • SET key value NX PX milliseconds命令在不存在key的情况下添加具有过期时间的key,为安全加锁提供支持。
  • Lua脚本和DEL命令为安全解锁提供可靠支撑。

代码实现

  • Maven依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  	<version>${your-spring-boot-version}</version>
</dependency>
Salin selepas log masuk
  • 配置文件

在application.properties增加以下内容,单机版Redis实例。

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
Salin selepas log masuk
  • 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;
    }
}
Salin selepas log masuk
  • 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;
    }
}
Salin selepas log masuk
  • 测试用例
@SpringBootTest
class RedisDistLockDemoApplicationTests {

    @Resource
    private RedisLock redisLock;

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

安全隐患

可能很多同学(也包括我)在日常工作中都是使用上面的实现方式,看似是稳妥的:

  • 使用set命令NXPX选项进行加锁,保证了加锁互斥,避免了死锁;
  • 使用lua脚本解锁,防止解除其他线程的锁;
  • 加锁、解锁命令都是原子操作;

其实以上实现的稳妥有个前提条件:单机版Redis、开启AOF持久化方式并设置appendfsync=always

但是在哨兵模式和集群模式下可能存在问题,为什么呢?

哨兵模式和集群模式基于主从架构,主从之间通过命令传播实现数据同步,而命令传播是异步的。

所以就存在主节点数据写入成功,在还未通知从节点情况下,主节点就宕机的可能。

当从节点通过故障转移提升为新的主节点后,其他线程就有机会重新加锁成功,导致不满足分布式锁的互斥条件。

官方RedLock


集群模式下,若集群所有节点稳定运行,不出现故障转移的情况下,安全性是有保障的。但是,没有什么系统能够保证100%稳定,基于Redis的分布式锁必须考虑容错。

由于主从同步基于异步复制原理,所以哨兵模式和集群模式天生无法满足此条件。为此,Redis作者专门提出了一种解决方案——RedLock(Redis Distribute Lock)。

设计思路

根据官方文档的说明,把RedLock的设计思路进行介绍。

先说环境要求,需要N(N>=3)个独立部署的Redis实例,相互之间不需要主从复制、故障转移等技术。

为了获取锁,客户端将按照以下流程进行操作:

  • 获取当前时间(毫秒)作为开始时间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();
        }
    }
Salin selepas log masuk

Redisson封装的非常好,我们可以像使用Java内置的锁一样去使用,代码简洁的不能再少了。关于Redisson源码的分析,网上有很多文章大家可以找找看。

全文总结


分布式锁是我们研发过程中常用的的一种解决并发问题的方式,Redis是只是一种实现方式。

关键的是要弄清楚加锁、解锁背后的原理,以及实现分布式锁需要解决的核心问题,同时考虑我们所采用的中间件有什么特性可以支撑。了解这些后,实现起来就不是什么问题了。

学习了RedLock的思想,我们是不是也可以在自己的应用程序内实现了分布式锁呢?欢迎沟通!

更多编程相关知识,请访问:编程入门!!

Atas ialah kandungan terperinci 使用Redis实现一个安全可靠的分布式锁. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn

Alat AI Hot

Undresser.AI Undress

Undresser.AI Undress

Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover

AI Clothes Remover

Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Undress AI Tool

Undress AI Tool

Gambar buka pakaian secara percuma

Clothoff.io

Clothoff.io

Penyingkiran pakaian AI

Video Face Swap

Video Face Swap

Tukar muka dalam mana-mana video dengan mudah menggunakan alat tukar muka AI percuma kami!

Alat panas

Notepad++7.3.1

Notepad++7.3.1

Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina

SublimeText3 versi Cina

Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1

Hantar Studio 13.0.1

Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6

Dreamweaver CS6

Alat pembangunan web visual

SublimeText3 versi Mac

SublimeText3 versi Mac

Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Cara Membina Mod Kluster Redis Cara Membina Mod Kluster Redis Apr 10, 2025 pm 10:15 PM

Mod Redis cluster menyebarkan contoh Redis ke pelbagai pelayan melalui sharding, meningkatkan skalabilitas dan ketersediaan. Langkah -langkah pembinaan adalah seperti berikut: Buat contoh Redis ganjil dengan pelabuhan yang berbeza; Buat 3 contoh sentinel, memantau contoh redis dan failover; Konfigurasi fail konfigurasi sentinel, tambahkan pemantauan maklumat contoh dan tetapan failover; Konfigurasi fail konfigurasi contoh Redis, aktifkan mod kluster dan tentukan laluan fail maklumat kluster; Buat fail nodes.conf, yang mengandungi maklumat setiap contoh Redis; Mulakan kluster, laksanakan perintah Buat untuk membuat kluster dan tentukan bilangan replika; Log masuk ke kluster untuk melaksanakan perintah maklumat kluster untuk mengesahkan status kluster; buat

Cara membersihkan data redis Cara membersihkan data redis Apr 10, 2025 pm 10:06 PM

Cara Mengosongkan Data Redis: Gunakan perintah Flushall untuk membersihkan semua nilai utama. Gunakan perintah flushdb untuk membersihkan nilai utama pangkalan data yang dipilih sekarang. Gunakan Pilih untuk menukar pangkalan data, dan kemudian gunakan FlushDB untuk membersihkan pelbagai pangkalan data. Gunakan perintah DEL untuk memadam kunci tertentu. Gunakan alat REDIS-CLI untuk membersihkan data.

Cara Membaca Gilir Redis Cara Membaca Gilir Redis Apr 10, 2025 pm 10:12 PM

Untuk membaca giliran dari Redis, anda perlu mendapatkan nama giliran, membaca unsur -unsur menggunakan arahan LPOP, dan memproses barisan kosong. Langkah-langkah khusus adalah seperti berikut: Dapatkan nama giliran: Namakannya dengan awalan "giliran:" seperti "giliran: my-queue". Gunakan arahan LPOP: Keluarkan elemen dari kepala barisan dan kembalikan nilainya, seperti LPOP Queue: My-Queue. Memproses Baris kosong: Jika barisan kosong, LPOP mengembalikan nihil, dan anda boleh menyemak sama ada barisan wujud sebelum membaca elemen.

Cara menggunakan perintah redis Cara menggunakan perintah redis Apr 10, 2025 pm 08:45 PM

Menggunakan Arahan Redis memerlukan langkah -langkah berikut: Buka klien Redis. Masukkan arahan (nilai kunci kata kerja). Menyediakan parameter yang diperlukan (berbeza dari arahan ke arahan). Tekan Enter untuk melaksanakan arahan. Redis mengembalikan tindak balas yang menunjukkan hasil operasi (biasanya OK atau -r).

Cara menggunakan kunci redis Cara menggunakan kunci redis Apr 10, 2025 pm 08:39 PM

Menggunakan REDIS untuk mengunci operasi memerlukan mendapatkan kunci melalui arahan SETNX, dan kemudian menggunakan perintah luput untuk menetapkan masa tamat tempoh. Langkah-langkah khusus adalah: (1) Gunakan arahan SETNX untuk cuba menetapkan pasangan nilai utama; (2) Gunakan perintah luput untuk menetapkan masa tamat tempoh untuk kunci; (3) Gunakan perintah DEL untuk memadam kunci apabila kunci tidak lagi diperlukan.

Cara membaca kod sumber redis Cara membaca kod sumber redis Apr 10, 2025 pm 08:27 PM

Cara terbaik untuk memahami kod sumber REDIS adalah dengan langkah demi langkah: Dapatkan akrab dengan asas -asas Redis. Pilih modul atau fungsi tertentu sebagai titik permulaan. Mulakan dengan titik masuk modul atau fungsi dan lihat baris kod mengikut baris. Lihat kod melalui rantaian panggilan fungsi. Berhati -hati dengan struktur data asas yang digunakan oleh REDIS. Kenal pasti algoritma yang digunakan oleh Redis.

Cara menggunakan baris arahan redis Cara menggunakan baris arahan redis Apr 10, 2025 pm 10:18 PM

Gunakan alat baris perintah redis (redis-cli) untuk mengurus dan mengendalikan redis melalui langkah-langkah berikut: Sambungkan ke pelayan, tentukan alamat dan port. Hantar arahan ke pelayan menggunakan nama arahan dan parameter. Gunakan arahan bantuan untuk melihat maklumat bantuan untuk arahan tertentu. Gunakan perintah berhenti untuk keluar dari alat baris arahan.

Cara Mengkonfigurasi Masa Pelaksanaan Skrip Lua di Centos Redis Cara Mengkonfigurasi Masa Pelaksanaan Skrip Lua di Centos Redis Apr 14, 2025 pm 02:12 PM

Pada sistem CentOS, anda boleh mengehadkan masa pelaksanaan skrip LUA dengan mengubah fail konfigurasi REDIS atau menggunakan arahan REDIS untuk mengelakkan skrip jahat daripada memakan terlalu banyak sumber. Kaedah 1: Ubah suai fail konfigurasi Redis dan cari fail konfigurasi Redis: Fail konfigurasi Redis biasanya terletak di /etc/redis/redis.conf. Edit Fail Konfigurasi: Buka fail konfigurasi menggunakan editor teks (seperti Vi atau nano): sudovi/etc/redis/redis.conf Tetapkan had masa pelaksanaan skrip lua: Tambah atau ubah suai baris berikut dalam fail konfigurasi untuk menetapkan masa pelaksanaan maksimum skrip lua (unit: milidor)

See all articles