Redis分布式锁

134 阅读2分钟

分布式锁的应用场景

  • 互联网秒杀
  • 抢优惠券
  • 接口幂等性校验

首先来看分布式锁解决超卖的场景

package com.zhuge;

import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
public class IndexController {

    @Autowired
    private Redisson redisson;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/deduct_stock")
    public String deductStock() {
        String lockKey = "lock:product_101";
        //Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
        //stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
        /*String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
        if (!result) {
            return "error_code";
        }*/
        //获取锁对象
        RLock redissonLock = redisson.getLock(lockKey);
        //加分布式锁
        redissonLock.lock();  //  .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            /*if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }*/
            //解锁
            redissonLock.unlock();
        }


        return "end";
    }


    @RequestMapping("/redlock")
    public String redlock() {
        String lockKey = "product_001";
        //这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
        RLock lock1 = redisson.getLock(lockKey);
        RLock lock2 = redisson.getLock(lockKey);
        RLock lock3 = redisson.getLock(lockKey);

        /**
         * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
         */
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        try {
            /**
             * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
             * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
             */
            boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
            if (res) {
                //成功获得锁,在这里处理业务
            }
        } catch (Exception e) {
            throw new RuntimeException("lock fail");
        } finally {
            //无论如何, 最后都要解锁
            redLock.unlock();
        }

        return "end";
    }

}

在上面这段代码中当多个线程来同时扣减库存,对stock进行减1操作,假设初始值:stock = 50 多个线程都来进行库存扣减操作,stock = 49,但是正常逻辑是库存应该扣50 - 2 = 48,因此在高并发场景下就有可能出现超卖的现象。用Sychronized锁或者JDk层面的锁只能实现单机锁库存,而在高并发场景下是没有办法实现的。在分布式环境下肯定需要用到分布式锁----实现方式有之前讲到过的setnx

 Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lockKey","haha");

Redis中存在lockKey这个key值就不会执行上面的语句,如果不存在就会执行。 进行判断:

if(!result){//true,返回状态码
return "1001";//
}

只让一个线程执行下面的代码。

Redission锁续期

线程会去判断当前执行的业务逻辑是否结束,未结束的话Redissn分布式锁框架有个看门狗机制,会给当前线程加的锁进行续期。

lua脚本会将Redis的多条命令封装成一个脚本,这个脚本命令是原子性的-----redis单线程

Redis缓存设计

一. 缓存击穿:访问某个热点key的时候,在缓存中失效,一下子这些用户请求都直接打到数据库上,可能会造成数据库瞬间压力过大而直接挂掉。

  • 要保证同一时刻只能有一个请求访问某个productID的数据库商品信息,加锁的方式实现上面的功能,接着把从数据库中查询到的结果重新放入缓存中。
  • 在key快要过期的时候给key自动续期,重新设置过期时间。在很多请求第三方平台接口的时候,往往需要先调用一个获取token的接口,然后用这个token作为参数,请求真正的业务接口,一般获取到的token是有有效期的,比如24小时之后失效,如果每次请求对方的业务接口,都要先调用一次获取token的接口,显然比较麻烦而且性能不太好,这时候我们可以把第一次获取到的token缓存起来,请求对方业务接口时从缓存中获取token,同时有一个job每隔一段时间,比如每隔12个小时请求一次获取token的接口,不停刷新token,重新获取刷新token的时间
  • 此外对于很多热点key可以设置永不过期,比如参与秒杀的热门商品,由于这类商品id并不多,在缓存中国我们可以不设置过期时间,在秒杀活动开始前先用一个程序在数据库中查询商品数据,提前放入缓存中预热,秒杀活动结束之后手动删除缓存即可

二. 缓存穿透:用户恶意攻击,缓存和数据库中都没有数据。缓存null值、布隆过滤器

三. 缓存雪崩:缓存击穿说的是某一个热门key失效了,而缓存雪崩说的是多个热门key同时失效,导致大量的请求访问数据库,而数据库扛不住压力,而宕机,从而导致其他的服务器宕机。缓器服务器宕机了----机器硬件问题,或者是机房网络原因造成了整个缓存的不可用。归根结底都是当量的请求,透过缓存,直接打到数据库上

  • 避免缓存同时失效,给key设置不同的过期时间。可以在过期时间商加一个160s的随机数,即:
实际过期时间 = 设置的过期时间 + 160s的随机数

这样在高并发的环境下,多个请求同时设置过期时间 ,由于有随机数的存在,而不会导致多个热点key同时过期的情况,在前期做系统设计时,可以做一些高可用的架构

image.png

如果使用了redis可以使用哨兵模式或者集群模式,避免因为单节点故障导致这个redis服务不可用的情况,使用哨兵模式之后,当某个master几点下线时自动将该master节点下的某个slave服务升级为master服务,替代已下线的master服务处理请求。

如果做了高可用架构,redis服务还是挂了该怎么办呢?这个时候可以考虑== ==服务降级====配置一些默认的兜底数据 ,

程序中有一个全局开关,比如有是个请求在最近一分钟内从redis获取数据失败,则全局开关打开直接从配置中心获取默认的数据,当然,这个时候还需要有一个job每个一定的时间去redis中获取数据,如果在一分钟内可以获取到两次数据(这个参数可以自己定义),则把全局开关关闭,后面来的请求又可以正常从redis中获取数据了。该方案根据实际业务场景设置

image.png