Rumah  >  Artikel  >  pangkalan data  >  Ingat kemalangan besar yang disebabkan oleh kunci yang diedarkan Redis untuk mengelakkan perangkap pada masa hadapan!

Ingat kemalangan besar yang disebabkan oleh kunci yang diedarkan Redis untuk mengelakkan perangkap pada masa hadapan!

Java学习指南
Java学习指南ke hadapan
2023-07-26 16:25:281329semak imbas

Kata Pengantar

Menggunakan kunci yang diedarkan berdasarkan Redis bukanlah perkara baharu hari ini. Artikel ini terutamanya berdasarkan analisis dan penyelesaian kemalangan yang disebabkan oleh kunci yang diedarkan redis dalam projek sebenar kami.

Latar Belakang: Pesanan tergesa-gesa dalam projek kami diselesaikan menggunakan kunci yang diedarkan.

Pernah sekali, operasi mengadakan acara jualan tergesa-gesa untuk Feitian Moutai, dengan stok 100 botol, tetapi ia telah terlebih jual! Anda tahu, kekurangan Feitian Moutai di bumi ini! ! ! Kemalangan itu diklasifikasikan sebagai kemalangan besar P0... kami hanya boleh menerimanya dengan tenang. Keseluruhan persembahan pasukan projek telah ditolak~~

Selepas kemalangan, CTO menamakan saya dan meminta saya untuk memimpin bertanggungjawab untuk menanganinya, okey, caj~

Tempat kemalangan

Selepas beberapa pemahaman, saya belajar ini Keadaan ini tidak pernah berlaku dalam antara muka aktiviti pembelian tergesa-gesa sebelum ini, tetapi mengapa ia terlebih jual kali ini?

Alasannya ialah: produk jualan tergesa-gesa sebelum ini bukanlah barangan yang terhad, tetapi acara ini sebenarnya Feitian Moutai Melalui analisis data yang terkubur, pada dasarnya anda boleh bayangkan keghairahan acara itu. Tidak banyak yang perlu diperkatakan, mari kita pergi terus ke kod teras, dan bahagian sulit telah diproses dengan kod pseudo. . .

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId;
    try {
        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);
        if (lockFlag) {
            // HTTP请求用户服务进行用户相关的校验
            // 用户活动校验
            
            // 库存校验
            Object stock = redisTemplate.opsForHash().get(key+":info", "stock");
            assert stock != null;
            if (Integer.parseInt(stock.toString()) <= 0) {
                // 业务异常
            } else {
                redisTemplate.opsForHash().increment(key+":info", "stock", -1);
                // 生成订单
                // 发布订单创建成功事件
                // 构建响应VO
            }
        }
    } finally {
        // 释放锁
        stringRedisTemplate.delete("key");
        // 构建响应VO
    }
    return response;
}

Kod di atas menggunakan masa tamat tempoh kunci yang diedarkan selama 10 saat untuk memastikan masa pelaksanaan yang mencukupi untuk logik perniagaan, blok pernyataan cuba-akhir digunakan untuk memastikan kunci akan dilepaskan tepat pada masanya. Inventori juga disahkan dalam kod perniagaan. Nampak sangat selamat~ Jangan risau, teruskan analisis.

"

Cadangkan gudang untuk tutorial asas SpringBoot yang ditulis oleh DD, saya ingin membantu anda: https://gitee.com/didispace/SpringBoot-Learning/tree/master/2.1.x

Punca kemalangan

Acara jualan tergesa-gesa Feitian Moutai menarik sejumlah besar pengguna baharu untuk memuat turun dan mendaftar APP kami Antaranya, terdapat ramai peminat bulu yang menggunakan kaedah profesional untuk mendaftarkan pengguna baharu untuk menuai bulu. Sudah tentu, sistem pengguna kami telah mengambil langkah berjaga-jaga terlebih dahulu Akses kepada pelbagai kemahiran seperti pengesahan komputer manusia Alibaba Cloud, pengesahan tiga faktor dan sistem kawalan risiko yang dibangunkan sendiri telah menyekat sejumlah besar pengguna haram~

Tetapi ia adalah. juga kerana ini pengguna boleh menikmatinya. Perkhidmatan ini berada di bawah beban operasi yang tinggi

🎜Pada permulaan aktiviti jualan tergesa-gesa, sejumlah besar permintaan pengesahan pengguna melanda perkhidmatan pengguna, mengakibatkan kelewatan respons yang singkat. gerbang perkhidmatan pengguna, dan masa tindak balas bagi sesetengah permintaan melebihi 10 saat, bagaimanapun, disebabkan masa respons permintaan HTTP, kami menetapkannya kepada 30s, yang menyebabkan antara muka disekat dalam kawasan pengesahan pengguna kunci yang diedarkan telah tamat tempoh Pada masa ini, permintaan baharu boleh mendapatkan kunci apabila mereka masuk. Maksudnya, kunci ditimpa Selepas antara muka yang disekat ini dilaksanakan, logik pelepas kunci akan dilaksanakan, yang melepaskan kunci. benang lain, menyebabkan permintaan baharu bersaing untuk mendapatkan kunci. Ini benar-benar kitaran yang teruk.🎜

Pada masa ini, kita hanya boleh bergantung pada pengesahan inventori, tetapi pengesahan inventori bukan bukan atom Ia menggunakan kaedah dapatkan dan bandingkan Tragedi terlebih jual berlaku~~~

Analisis kemalangan

Setelah berhati-hati. analisis, boleh didapati bahawa antara muka snap-up ini mempunyai risiko keselamatan yang serius dalam senario konkurensi yang tinggi, terutamanya tertumpu di tiga tempat:

Tiada toleransi kesalahan risiko sistem lain

Disebabkan perkhidmatan pengguna yang ketat, gerbang A respons tertunda, tetapi tiada cara untuk menanganinya, adalah pencetus untuk terlebih jual.

Kunci teragih yang kelihatan selamat sebenarnya tidak selamat sama sekali

Walaupun kaedah set key value [EX seconds] [PX milliseconds] [NX|XX] diguna pakai, jika benang A mengambil masa yang lama untuk dilaksanakan dan tidak mempunyai masa untuk dilepaskan, kunci akan tamat tempoh, dan benang B boleh memperoleh ia pada masa ini Ke kunci. Apabila benang A menyelesaikan pelaksanaan dan melepaskan kunci, kunci benang B sebenarnya dilepaskan.

Pada masa ini, benang C boleh memperoleh kunci semula Pada masa ini, jika benang B selesai melaksanakan dan melepaskan kunci, ia sebenarnya kunci yang ditetapkan oleh benang C yang dilepaskan. Ini adalah punca langsung terlebih jual.

Pengesahan inventori bukan atom

非原子性的库存校验导致在并发场景下,库存校验的结果不准确。这是超卖的根本原因。

通过以上分析,问题的根本原因在于库存校验严重依赖了分布式锁。因为在分布式锁正常set、del的情况下,库存校验是没有问题的。但是,当分布式锁不安全可靠的时候,库存校验就没有用了。

解决方案

知道了原因之后,我们就可以对症下药了。

实现相对安全的分布式锁

相对安全的定义:set、del是一一映射的,不会出现把其他现成的锁del的情况。从实际情况的角度来看,即使能做到set、del一一映射,也无法保障业务的绝对安全。

因为锁的过期时间始终是有界的,除非不设置过期时间或者把过期时间设置的很长,但这样做也会带来其他问题。故没有意义。

要想实现相对安全的分布式锁,必须依赖key的value值。在释放锁的时候,通过value值的唯一性来保证不会勿删。我们基于LUA脚本实现原子性的get and compare,如下:

public void safedUnLock(String key, String val) {
    String luaScript = "local in = ARGV[1] local curr=redis.call(&#39;get&#39;, KEYS[1]) if in==curr then redis.call(&#39;del&#39;, KEYS[1]) end return &#39;OK&#39;"";
    RedisScript<String> redisScript = RedisScript.of(luaScript);
    redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singleton(val));
}

我们通过LUA脚本来实现安全地解锁。

实现安全的库存校验

如果我们对于并发有比较深入的了解的话,会发现想 get and compare/ read and save 等操作,都是非原子性的。如果要实现原子性,我们也可以借助LUA脚本来实现。

但就我们这个例子中,由于抢购活动一单只能下1瓶,因此可以不用基于LUA脚本实现而是基于redis本身的原子性。原因在于:

// redis会返回操作之后的结果,这个过程是原子性的
Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);

发现没有,代码中的库存校验完全是“画蛇添足”。

改进之后的代码

经过以上的分析之后,我们决定新建一个DistributedLocker类专门用于处理分布式锁。

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId();
    String val = UUID.randomUUID().toString();
    try {
        Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);
        if (!lockFlag) {
            // 业务异常
        }

        // 用户活动校验
        // 库存校验,基于redis本身的原子性来保证
        Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
        if (currStock < 0) { // 说明库存已经扣减完了。
            // 业务异常。
            log.error("[抢购下单] 无库存");
        } else {
            // 生成订单
            // 发布订单创建成功事件
            // 构建响应
        }
    } finally {
        distributedLocker.safedUnLock(key, val);
        // 构建响应
    }
    return response;
}

深度思考

分布式锁有必要么

改进之后,其实可以发现,我们借助于redis本身的原子性扣减库存,也是可以保证不会超卖的。对的。但是如果没有这一层锁的话,那么所有请求进来都会走一遍业务逻辑,由于依赖了其他系统,此时就会造成对其他系统的压力增大。这会增加的性能损耗和服务不稳定性,得不偿失。基于分布式锁可以在一定程度上拦截一些流量。

分布式锁的选型

有人提出用RedLock来实现分布式锁。RedLock的可靠性更高,但其代价是牺牲一定的性能。在本场景,这点可靠性的提升远不如性能的提升带来的性价比高。如果对于可靠性极高要求的场景,则可以采用RedLock来实现。

再次思考分布式锁有必要么

由于bug需要紧急修复上线,因此我们将其优化并在测试环境进行了压测之后,就立马热部署上线了。实际证明,这个优化是成功的,性能方面略微提升了一些,并在分布式锁失效的情况下,没有出现超卖的情况。

然而,还有没有优化空间呢?有的!

由于服务是集群部署,我们可以将库存均摊到集群中的每个服务器上,通过广播通知到集群的各个服务器。网关层基于用户ID做hash算法来决定请求到哪一台服务器。这样就可以基于应用缓存来实现库存的扣减和判断。性能又进一步提升了!

// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全
private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();
// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap
private static Map<Long, AtomicInteger> SECKILL_STOCK_MAP = new HashMap<>();

...

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;

    Long seckillId = request.getSeckillId();
    if(!SECKILL_FLAG_MAP.get(requestseckillId)) {
        // 业务异常
    }
     // 用户活动校验
     // 库存校验
    if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {
        SECKILL_FLAG_MAP.put(seckillId, false);
        // 业务异常
    }
    // 生成订单
    // 发布订单创建成功事件
    // 构建响应
    return response;
}

通过以上的改造,我们就完全不需要依赖redis了。性能和安全性两方面都能进一步得到提升!

当然,此方案没有考虑到机器的动态扩容、缩容等复杂场景,如果还要考虑这些话,则不如直接考虑分布式锁的解决方案。

Ringkasan

Berlebihan barang yang terhad pastinya satu kemalangan besar. Jika kuantiti terlebih jual adalah besar, ia malah akan memberi kesan operasi dan kesan sosial yang sangat serius pada platform. Selepas kemalangan ini, saya menyedari bahawa tiada baris kod dalam projek boleh dipandang ringan, jika tidak, dalam sesetengah senario, kod yang biasanya berfungsi ini akan menjadi pembunuh maut!

Bagi pemaju, semasa mereka bentuk pelan pembangunan, pelan itu mesti dipertimbangkan dengan teliti. Bagaimanakah kita boleh mempertimbangkan rancangan itu dengan teliti? Hanya terus belajar!

Atas ialah kandungan terperinci Ingat kemalangan besar yang disebabkan oleh kunci yang diedarkan Redis untuk mengelakkan perangkap pada masa hadapan!. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:Java学习指南. Jika ada pelanggaran, sila hubungi admin@php.cn Padam