redis与分布式锁浅谈

news2024/11/7 1:31:22

redis与分布式锁浅谈

1.高并发下缓存失效问题

1.1 缓存穿透:

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决: null结果缓存,并加入短暂过期时间

1.2 缓存雪崩

缓存雪崩: 缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
风险: DB瞬时压力过重雪崩
解决: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

1.3缓存穿透

缓存穿透: 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
风险: 数据库瞬时压力增大,最终导致崩溃
解决: 加锁大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db

2.分布式锁

分布式锁需要解决的问题:
问题1: setnx占好了位,业务代码异常或者程序在页面过程中宕机,没有执行删除锁逻辑,这就造成了死锁
解决:设置锁的自动过期,即使没有删除,会自动删除

问题2:setnx设置好,正要去设置过期时间,宕机。又死锁了。
解决: 设置过期时间和占位必须是原子的。redis支持使用setnx ex命令

问题3: 删除锁直接删除??? 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决: 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除

问题4: 如果正好判断是当前值,正要删除锁的时候,锁已经过期, 别人已经设置到了新的值。那么我们删除的是别人的锁
解决: 删除锁必须保证原子性。使用redis+Lua脚本完成。 String script = “if redis.call(‘get’,
KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”;
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。 更难的事情,锁的自动续期

背景:最近遇到一个生产问题,分布式部署了十几条服务器,有个业务过期的定时任务会每天发邮件提醒用户,然而用户最近反馈,每天收到好几封提醒邮件,于是排查多发的原因。这个分布式锁很重要,大概率是没锁住

2.1 RedisTemplate(或stringRedisTemplate) 实现

 public void doSendEmail() {

        //1.生成随机数
        String uuid = UUID.randomUUID().toString();
        //2.设置分布式锁,并设置过期时间120秒
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock_grant_expire_notice", uuid, 120, TimeUnit.SECONDS);
        //3.获取到锁
        try {
            if (lock) {
                //4.抢到锁把计数归零 RECURSIVE_CALL_TIMES 是定义在类下面的常量 private int RECURSIVE_CALL_TIMES = 0;
                RECURSIVE_CALL_TIMES = NumberUtils.INTEGER_ZERO;
                //5.获取邮件是否已发送标识
                Object isSendFlag = redisTemplate.opsForValue().get("mail_is_send_flag");
                //6.没有值,就是未发送邮件
                if (isSendFlag == null || StringUtils.isEmpty(isSendFlag)) {

                    //加锁成功...执行业务
                    //7.发送邮件的真正业务
                    //sendMail();

                    redisTemplate.opsForValue().set("mail_is_send_flag", UUID.randomUUID().toString(), 23 * 60 * 60, TimeUnit.SECONDS);
                } else {
                    //8.邮件已发送,无需重复发送
                    log.info("mail already send,there's no need to send it twice");
                }
                //9.释放分布式锁,对比uuid值是为了只删除自己的锁,且对比值和删锁是原子操作
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock_grant_expire_notice"), uuid);

            } else {
                //10.未获取到分布式锁,尝试自璇,每10秒递归调用一次,尝试获取分布式锁,最多尝试5次
                Thread.sleep(10000);
                if (RECURSIVE_CALL_TIMES <= 5) {
                    RECURSIVE_CALL_TIMES += NumberUtils.INTEGER_ONE;
                    doSendEmail();
                }
            }
        } catch (Exception e) {
            log.error("execute send mail fail,message:" + e);
        }
    }

2.2 JedisCluster 实现

Jedis初始化类,连接redis,以及一些常用的方法

@Component("redisClusterConfig")
public class RedisClusterConfig {

    private static Log log = LogFactory.getLog(RedisClusterConfig.class);
    private volatile JedisCluster jedisCluster;

    public RedisClusterConfig() {
        initCluster();
    }

    public JedisCluster getClusterResource() {
        initCluster();
        return jedisCluster;
    }

    private void initCluster() {
        if (null != jedisCluster) {
            return;
        }
        synchronized (this) {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            //redis的相关配置文件(把redis的相关信息配置在一个文件中,读取文件)
            Properties properties = PropertyUtils.loadProperty("redis-context.properties");


            jedisPoolConfig.setMaxTotal(PropertyUtils.getIntegerProperty(properties, "redis.pool.maxTotal", 50));
            jedisPoolConfig.setMaxIdle(PropertyUtils.getIntegerProperty(properties, "redis.pool.maxIdle", 20));
            jedisPoolConfig.setMinIdle(PropertyUtils.getIntegerProperty(properties, "redis.pool.minIdle", 10));
            jedisPoolConfig.setMaxWaitMillis(PropertyUtils.getIntegerProperty(properties, "redis.pool.maxWaitMillis", 10000));
            jedisPoolConfig.setTimeBetweenEvictionRunsMillis(PropertyUtils.getIntegerProperty(properties, "redis.pool.timeBetweenEvictionRunsMills", 60000));

            jedisPoolConfig.setTestOnBorrow(false);
            jedisPoolConfig.setTestOnReturn(false);
            jedisPoolConfig.setTestWhileIdle(true);

            String clusterHost = properties.getProperty("redis.pool.cluster.host");
            int timeout = PropertyUtils.getIntegerProperty(properties, "redis.timeout", 2000);
            int sockettime = PropertyUtils.getIntegerProperty(properties, "redis.pool.cluster.sockettimeout", 2000);
            int maxAttempts = PropertyUtils.getIntegerProperty(properties, "redis.pool.cluster.maxAttempts", 2000);
            String password = properties.getProperty("redis.pool.cluster.password");

            Set<HostAndPort> nodes = new HashSet<>();
            String[] hosts = clusterHost.split(",");
            for (String ipPort : hosts) {
                String[] ipPortArr = ipPort.split(":");
                String ip = ipPortArr[0];
                int port = Integer.parseInt(ipPortArr[1]);
                nodes.add(new HostAndPort(ip, port));
            }
            jedisCluster = new JedisCluster(nodes, timeout, sockettime, maxAttempts, password, jedisPoolConfig);
        }
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        if (jedisCluster != null) {
            jedisCluster.close();
        }
    }

    /**
     * 获取分布式锁
     *
     * @param lockKey
     * @param value
     * @param expireTime
     * @return
     */
    public boolean getLock(String lockKey, String value, int expireTime) {
        String LOCK_SUCCESS = "OK";
        boolean clusterRtnValue = false;
        lockKey = replace4set(lockKey);
        JedisCluster clusterResource = getClusterResource();
        try {
            String result = clusterResource.set(lockKey, value, "NX", "EX", expireTime);
            if (LOCK_SUCCESS.equalsIgnoreCase(result)) {
                clusterRtnValue = true;
            }
        } catch (Exception e) {
            log.error("exception:" + e);
        }
        return clusterRtnValue;
    }

    public boolean releaseLock(String lockKey, String value) {
        Long RELEASE_SUCCESS = 1L;
        boolean clusterRtnValue = false;
        lockKey = replace4set(lockKey);
        JedisCluster clusterResource = getClusterResource();

        try {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = clusterResource.eval(script, Collections.singletonList(lockKey), Collections.singletonList(value));
            if (RELEASE_SUCCESS.equals(result)) {
                clusterRtnValue = true;
            }

        } catch (Exception e) {
            log.error("exception:" + e);
        }
        return clusterRtnValue;
    }

    public String set(String key, String value) {
        String clusterRtnValue = null;
        JedisCluster clusterResource = getClusterResource();

        try {
            clusterRtnValue = clusterResource.set(key, value);
        } catch (Exception e) {
            log.error("exception:" + e);
        }
        return clusterRtnValue;
    }

    public String setEx(String key, String value, int seconds) {
        String clusterRtValue = null;
        key = replace4set(key);
        JedisCluster clusterResource = getClusterResource();
        try {
            clusterRtValue = clusterResource.setex(key, seconds, value);
        } catch (Exception e) {
            log.error("exception:" + e);
        }
        return clusterRtValue;
    }

    public String get(String key) {
        String clusterRtValue = null;
        key = replace4set(key);
        JedisCluster clusterResource = getClusterResource();

        try {
            clusterRtValue = clusterResource.get(key);
        } catch (Exception e) {
            log.error("exception:" + e);
        }
        return clusterRtValue;
    }


    private String replace4set(String str) {
        return str.replaceAll("\\{", "[").replaceAll("}", "]");
    }
}

redis-context.properties配置内容

redis.pool.maxTotal=50
redis.pool.maxIdle=20
redis.pool.minIdle=10
redis.pool.maxWaitMillis=10000
redis.pool.timeBetweenEvictionRunsMills=60000

redis.pool.cluster.host=192.168.10.128:6379,192.168.10.131:6379
redis.timeout=2000
redis.pool.cluster.sockettimeout=2000
redis.pool.cluster.maxAttempts=2000
redis.pool.cluster.password=420188

分布式锁发送邮件

 public void doSendEmail() {
        //1.生成随机数
        String redisLockValue = UUID.randomUUID().toString();
        //2.设置分布式锁,并设置过期时间120秒
        boolean lock = redisClusterConfig.getLock("lock_grant_expire_notice", redisLockValue, 120);
        try {
            if (lock) {
                //抢到锁把计数归零
                RECURSIVE_CALL_TIMES = NumberUtils.INTEGER_ZERO;
                //获取邮件是否已发送标识
                String isSendFlag = redisClusterConfig.get("mail_is_send_flag");
                //未发送
                if (StringUtils.isBlank(isSendFlag)) {
                    //发送邮件的真正业务
                    //sendMail();
                    redisClusterConfig.setEx("mail_is_send_flag", UUID.randomUUID().toString(), 23 * 60 * 60);

                } else {
                    //已发送
                    log.info("mail already send,there's no need to send it twice");
                }
                //释放分布式锁
                redisClusterConfig.releaseLock("lock_grant_expire_notice", redisLockValue);
                return;
            } else {
                //未获取到分布式锁
                Thread.sleep(10000);
                if (RECURSIVE_CALL_TIMES <= 5) {
                    RECURSIVE_CALL_TIMES += NumberUtils.INTEGER_ONE;
                    doSendEmail();
                }
            }

        } catch (Exception e) {
            log.error("execute send mail fail,message:" + e);
        }
    }

2.3 Redisson 实现

以上两种方法都差不多,但无法解决redis续期问题,如果业务执行时间超过了分布式锁的过期时间,会有问题。当然 把分布式锁时间设置稍长一点一般也没什么大问题。redisson在业务未执行完会自动续期

在这里插入图片描述

public void doSendEmail() {
    //创建分布式锁
    RLock lock = redisson.getLock("lock_grant_expire_notice");
    try {
        //获取分布式锁(参数1:等待时间,参数2:过期时间 参数3:时间单位)
        if (lock.tryLock(0, 120000, TimeUnit.MILLISECONDS)) {
            //抢到锁把计数归零
            RECURSIVE_CALL_TIMES = NumberUtils.INTEGER_ZERO;
            //获取邮件是否已发送标识
            String isSendFlag = redisClusterConfig.get("mail_is_send_flag");
            //未发送
            if (StringUtils.isBlank(isSendFlag)) {
                //发送邮件的真正业务
                //sendMail();
                redisClusterConfig.setEx("mail_is_send_flag", UUID.randomUUID().toString(), 23 * 60 * 60);

            } else {
                //已发送
                log.info("mail already send,there's no need to send it twice");
            }
            //释放分布式锁
            lock.unlock();
            return;
        } else {
            //未获取到分布式锁
            Thread.sleep(10000);
            if (RECURSIVE_CALL_TIMES <= 5) {
                RECURSIVE_CALL_TIMES += NumberUtils.INTEGER_ONE;
                doSendEmail();
            }
        }

    } catch (Exception e) {
        log.error("execute send mail fail,message:" + e);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/624981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

windows禁用输入法

Rime 呼出菜单的快捷键 ctrl grave 跟 vs code 呼出底部命令行的快捷键冲突了&#xff0c;每次用 vs code 时都会用 ctrl space 将输入法禁用&#xff0c;让它变成一个圈叉&#xff1a; 由 [1]&#xff0c;这个快捷键是 windows 系统禁用输入法的快捷键&#xff0c;在 Setti…

实战干货——教你用Fiddler捕获HTTPS请求

目录 安装Fiddler 配置Fiddler 配置手机 iOS机安装证书 安全思考&#xff1f; 总结&#xff1a; 安装Fiddler 这里不特别说明了&#xff0c;网上搜索一大把&#xff0c;根据安装引导一步步安装即可。&#xff08;这里采用的是fiddler v4.6&#xff09; fiddler抓包视频教…

深入理解Linux虚拟内存管理(六)

系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核&#xff08;一&#xff09; 深入理解 Linux 内核&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;一&#xff09; Linux 设备驱动程序&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;三&#xf…

奇安信应急响应-Windows

处置思路方法和Linux是一致的&#xff0c; 系统命令&#xff0c; 有一些整蛊的就会锁定你&#xff0c;不让你用鼠标点击&#xff0c;就通过命令其打开就好 findstr命令跟linux一样查找关键字&#xff0c;图中就是hello关键字&#xff0c;然后.txt的文件&#xff0c; 我们可以…

(1)HTTP与RPC区别

定义 HTTP接口使用基于HTTP协议的URL传参调用RPC接口则基于远程过程调用 http是一种协议 &#xff0c;rpc是一种方法 RPC RPC服务基本架构包含了四个核心的组件&#xff0c;分别是Client、Server、Clent Stub以及Server Stub。 Client &#xff08;客户端&#xff09;&am…

【数据可视化】2D/3D动画

## 2D动画 - transform ◼ CSS3 transform属性允许你旋转&#xff0c;缩放&#xff0c;倾斜或平移给定元素。 ◼ Transform是形变的意思(通常也叫变换)&#xff0c;transformer就是变形金刚 ◼ 常见的函数transform function有: ---- 平移:translate(x, y) ---- 缩放:scale…

600万用户在用,中国版Access上市,Excel和WPS用户直呼:太棒了

中国版的Access到底有没有&#xff1f; 大家都知道微软的Access功能很强大&#xff0c;作为office里的一款数据库软件&#xff0c;不仅能帮助我们进行数据的分析和处理&#xff0c;而且再深入一点&#xff0c;还可以用VBA实现一些高级的用法。不仅国外有很多用户&#xff0c;就…

【C++】deque的用法

目录 一、容器适配器二、deque的介绍三、deque的使用及缺陷1、deque的构造函数2、deque的元素访问接口3、deque的 iterator的使用4、deque的增删查改4、deque的缺陷5、为什么选择deque作为stack和queue的底层默认容器 一、容器适配器 在了解deque前&#xff0c;我们先讲一讲什…

2023年,我被迫裸辞....

作为IT行业的大热岗位——软件测试&#xff0c;只要你付出了&#xff0c;就会有回报。说它作为IT热门岗位之一是完全不虚的。可能很多人回说软件测试是吃青春饭的&#xff0c;但放眼望去&#xff0c;哪个工作不是这样的呢&#xff1f;会有哪家公司愿意养一些闲人呢&#xff1f;…

STM32F4_RS485、RS232

目录 1. 485简介 2. 串口UART存在的问题 3. RS232协议 4. RS485协议 6. 硬件分析 7. 实验程序 7.1 main.c 7.2 RS485.c 7.3 RS485.h RS232的高电平1的逻辑为-5V~-15V&#xff0c;低电平0的逻辑为5V~15V。高电平和TTL的0~5V不兼容&#xff0c;传输的距离也不够长。 1. …

SpringCloud Eureka 的详细讲解及示意图-下

SpringCloud Eureka 服务注册与发现-下 搭建EurekaServer 集群- 实现负载均衡&故障容错 为什么需要集群Eureka Server 示意图 说明 1. 微服务RPC 远程服务调用最核心的是实现高可用 2. 如果注册中心只有1 个&#xff0c;它出故障&#xff0c;会导致整个服务环境不可用…

乘法器介绍

阵列乘法器 实现乘法的比较常用的方法是类似与手工计算乘法的方式&#xff1a; 对应的硬件结构就是阵列乘法器&#xff08;array multiplier&#xff09;它有三个功能&#xff1a;产生部分积&#xff0c;累加部分积和最终相加。 阵列乘法器的关键路径为(下图标出了两条可能的关…

Clion开发STM32之ESP8266系列(四)

前言 上一篇: Clion开发STM32之ESP8266系列(三) 本篇主要内容 实现esp8266需要实现的函数串口3中断函数的自定义&#xff08;这里没有使用HAL提供的&#xff09;封装esp8266服务端的代码和测试 正文 主要修改部分 核心配置头文件(添加一些宏定义) sys_core_conf.h文件中…

【报错】检索 COM 类工厂中 CLSID 为 {28E68F9A-8D75-11D1-8DC3-3C302A000000} 的组件失败错误

【报错】检索 COM 类工厂中 CLSID 为 {28E68F9A-8D75-11D1-8DC3-3C302A000000} 的组件失败错误 情况描述解决方法修改目标平台CPU类型下载组件文件复制到指定路径运行指定命令行程序 情况描述 在使用C#进行工控软件开发&#xff0c;需要连接通过OPC连接DCS系统时&#xff0c;需…

STM32--ESP8266物联网WIFI模块(贝壳物联)--温湿度数据上传服务器显示

本文适用于STM32F103C8T6等MCU&#xff0c;其他MCU可以移植&#xff0c;完整资源见文末链接 一、简介 随着移动物联网的发展&#xff0c;各场景下对于物联控制、数据上传、远程控制的诉求也越来越多&#xff0c;基于此乐鑫科技推出了便宜好用性价比极高的wifi物联模块——ESP…

PowerShell系列(五):PowerShell通过脚本方式运行笔记

目录 一、四种执行方式介绍 1、当前文件夹运行命令 2、直接指定完整文件路径执行 3、通过cmd命令直接执行 4、通过Windows计划任务执行PowerShell脚本 二、通过脚本方式执行命令的优势 往期回顾 PowerShell系列&#xff08;一&#xff09;&#xff1a;PowerShell介绍和cm…

Java 异常处理和最佳实践(含案例分析)

概述 最近在代码 CR 的时候发现一些值得注意的问题&#xff0c;特别是在对 Java 异常处理的时候&#xff0c;比如有的同学对每个方法都进行 try-catch&#xff0c;在进行 IO 操作时忘记在 finally 块中关闭连接资源等等问题。回想自己对 java 的异常处理也不是特别清楚&#x…

第一章 软件工程概论

文章目录 第一章 软件工程概论1. 软件危机1.1.1 软件危机的介绍1.1.2 产生软件危机的原因与软件本身特点有关软件开发与维护的方法不正确有关 1.1.3 消除软件危机的途径例题 软件工程1.2.1 软件工程的介绍1.2.2 软件工程的基本原理1.2.3 软件工程方法学1. 传统方法学2. 面向对象…

集群间 ssh 互信免密码登录失败处理

一、问题描述 某次GreePlum集群免密配置过程中&#xff0c;需要使用普通用户实现ssh免密登录&#xff0c;前方反馈root用户已可完成免密登录&#xff0c;但普通用户同样配置&#xff0c;未生效&#xff0c;提示需输入密码才可以。 现场环境&#xff1a; 二、问题分析处理 …

安卓packageinfo的知识点

PackageInfo类包含AndroidManifest.xml文件的信息。 一些常用的属性如下&#xff1a; 获得PackageInfo //获取指定包名的packageInfo&#xff0c;并且包含所有的内容提供者 val pack context.packageManager.getPackageInfo(context.packageName,PackageManager.GET_PROVIDE…