java中使用Redis实现分布式锁

news2025/3/12 19:49:17

前言

目前很多大型的互联网公司后端都采用了分布式架构来支撑前端应用,其中服务拆分就是分布式的一种体现,既然服务拆分了,那么多个服务协调工作就会出现一些资源竞争的情况。比如多个服务对同一个表中的数据进行处理。容易出现类似多线程的不同步问题。多线程不同步我们一般通过代码中的锁来强制同步执行。但是对于这种多个服务的,这种本地锁久显得无能为力了。

锁实现

锁的实现目的就是保证某个时间只允许一个持有锁的线程来操作一个对象。实现思路就是设置一个任何线程都可见可操作的对象(锁),通过控制对象的状态来让线程知道当前锁是否被人持有了。举个例子比如火车上的卫生间,就是通过列车上的灯的状态来标识厕所是否被占用,如果是红灯,表示厕所当前有人,其他人不可以开门。如果是绿灯,则表示当前厕所是空的,可以开门进去操作。那么对比着来看,厕所其实就是被锁定的资源,那个灯就是锁标识。当然也可以认为门上的锁是锁标识(其实本来就是个锁)。

基于redis的分布式锁

原理就是利用redis中string的key value,如果set key成功表示目标资源没人使用,如果没有set key失败或报错表示资源已经有人使用了。

加锁

如下:

image.png

很显然单纯的使用set key value这种方式是不可以行的,因为redis默认会进行覆盖。
mysql中建表的时候有一句 crate table if not exists test (xxxxxx); 同样的redis中也提供了类似的先判断在set的用法。

set key value nx 

其中nx 表示的就是 not exists的意思

image.png

那么使用这种方式貌似就可以达到标识资源被占用的目的,如果一个线程设置了某个key,其他线程再set这个key就会失败。

但是如果A线程加锁之后去执行业务,但是业务失败了,导致A线程没有解锁,那么其他线程就只能看一直等待,就比如有人去了厕所,完事儿之后在里面玩手机玩上头了一直不出来,这个时候厕所外的标识灯一直是红色,那么别人就只能一直等待。所以要加一个机制来避免这种问题,比如设置一个超时时间。据说国外有一个发明就是为了防止有人长时间占用厕所,将厕所墙壁设计成透明的,墙壁是一个特殊的材料制成,可以让人从里面看不到外面,外面也看不到里面,但是人在里面如果超过十分钟后,这种材料就会褪色,最后变成全透明的,新闻在这里https://www.bilibili.com/read/cv6156620/ 。同样的道理,为了防止持有锁的线程在执行业务的时候报错导致不能及时的释放锁,所以redis本身可以针对特定key设置过期时间,如果到期了自动释放锁。
命令如下:

expire lock 5

设置锁的过期时间为5秒
image.png

上图模拟了线程设置锁标识之后5秒之后其他线程可以获取锁。

但是,这样还有一个问题,在高并发环境下,两个命令分别执行不能保证原子性,所以继续优化
如下:

set lock 1 ex 5 nx

这条命令其实是将上面的两条命令糅合在一起,这样就可以保证获取锁的原子性了。

下面考虑另外一个问题,如果A持有锁的线程在执行一个比较耗时的业务,达到了锁的自动释放时间,但是工作任没有完成,这样锁就会被提前释放,然后B线程获取到了锁开始工作,在B线程工作还没有结束时,A线程工作结束了,于是调用了释放锁的操作,这样就会引起连续的错误反应,最终导致锁没有起到任何作用。于是继续优化:
解决方案一
程序员根据业务情况评估被锁保护的公共资源的执行耗时,适当的增减redis中key的自动释放时间。
解决方法二
加锁的方式不变,锁的自动释放时间设置可以略长一些,程序加锁的时候给redis中的value设置一个随机数,释放锁的时候根据随机数来判断当前锁是不是自己的。可以采用lua的方式来保证判断的和释放的原子性问题。如下:

if redis.call("get",KEYS[1]) == ARGV[1] 
 then 
   return redis.call("del",KEYS[1])  
 else   
  return 0  
end
解锁

解锁其实就是将key清除

del key

java代码实现:

锁的主体逻辑

package team.lcf.lock.redis.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;

/**
 * @program: LuoThinking-cloud
 * @description: 基于redis实现的分布式锁
 * 原理: 锁的基本思想都是确保当前只有一个线程可以操作某个程序,为了
 * 加锁:set lock:test true nx 5 ex
 * 释放锁:del lock:test 为了保证原子性,一般采用lua脚本实现
 * @author: Cheng Zhi
 * @create: 2022-06-13 21:42
 **/
public class RedisLock {

    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);

    private Jedis redis;

    /**
     * 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。
     */
    public static final String NX = "NX";

    /**
     * seconds — 以秒为单位设置 key 的过期时间,等效于EXPIRE key seconds
     */
    public static final String EX = "EX";

    /**
     * 调用set后的返回值
     */
    public static final String OK = "OK";

    /**
     * 默认请求锁的超时时间(ms 毫秒)
     */
    private static final long TIME_OUT = 100;

    /**
     * 默认锁的有效时间(s)
     */
    public static final int EXPIRE = 30;

    /**
     * 解锁的lua脚本
     */
    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call("get",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call("del",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    /**
     * 锁标志对应的key
     */
    private String lockKey;

    /**
     * 锁对应的值
     */
    private String lockValue;

    /**
     * 锁的有效时间(s)
     */
    private int expireTime = EXPIRE;

    /**
     * 请求锁的超时时间(ms)
     */
    private long timeout = TIME_OUT;

    /**
     * 锁标记
     */
    private boolean locked = false;

    final Random random = new Random();

    public int getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(int expireTime) {
        this.expireTime = expireTime;
    }

    public long getTimeout() {
        return timeout;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    /**
     * 构造方法
     * @param jedis
     * @param lockKey
     */
    public RedisLock(Jedis jedis, String lockKey) {
        this.redis = jedis;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * 自定义过期时间和超时时间
     * @param jedis
     * @param lockKey
     * @param expireTime
     * @param timeout
     */
    public RedisLock(Jedis jedis, String lockKey, int expireTime, long timeout) {
        this(jedis, lockKey);
        this.expireTime = expireTime;
        this.timeout = timeout;
    }
    /**
     * 默认加锁
     * @return
     */
    public boolean lock() {
        // 获取随机数作为key
        lockValue = UUID.randomUUID().toString();
        final String set = redis.set(lockKey, lockValue, new SetParams().nx().ex(expireTime));
        locked = OK.equalsIgnoreCase(set);
        return locked;
    }

    /**
     * 释放锁
     * @return
     */
    public boolean unLock() {

        if (locked) {
            List<String> keys = new ArrayList<>();
            keys.add(lockKey);
            List<String> values = new ArrayList<>();
            values.add(lockValue);
            Long result = (Long) redis.eval(UNLOCK_LUA, keys, values);

            locked = result == 0? true: false;
            if (locked) {
                logger.warn("分布式锁{}解锁失败", lockKey);
            }
        }

        return locked;
    }

    /**
     * 以阻塞的方式获取锁,一直尝试获取,直到取到为止
     * @return
     */
    public boolean tryLockBlock() {

        while(true) {
            if (lock()) {
                return locked;
            }
            // 每次请求等待一段时间
            sleep(10, 50000);
        }
    }

    /**
     * 在指定超时时间内获取
     * @return
     */
    public boolean tryLock() {
        // 请求锁超时时间,纳秒
        long _timeout = timeout * 1000000;
        // 系统当前时间,纳秒
        long nowTime = System.nanoTime();
        while ((System.nanoTime() - nowTime) < _timeout) {
            if (lock()) {
                locked = true;
                // 上锁成功结束请求
                return true;
            }
            // 每次请求等待一段时间
            sleep(10, 50000);
        }
        return locked;

    }

    /**
     * 线程等待时间
     *
     * @param millis 毫秒
     * @param nanos  纳秒
     */
    private void sleep(long millis, int nanos) {
        try {
            Thread.sleep(millis, random.nextInt(nanos));
        } catch (InterruptedException e) {
            logger.info("获取分布式锁休眠被中断:", e);
        }
    }
}

设计注解:

package team.lcf.lock;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 方法粒度锁
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {

    /**
     * 使用锁的类型
     * 默认使用的是redis实现
   * @return
     */
    public String LockType() default "redis";

    /**
     * 锁超时时间 ms
     * @return
     */
    public int timeOut() default 1000;

    /**
     * key过期时间(s) 默认为10s
     * @return
     */
    public int expiteTime() default 10;

}

注解的处理:

package team.lcf.lock.redis.impl;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import team.lcf.lock.Lock;

import java.lang.reflect.Method;

/**
 * @program: LuoThinking-cloud
 * @description: Redis分布式锁管理
 * @author: Cheng Zhi
 * @create: 2022-06-14 14:38
 **/
@Aspect // 声明为aop
@Component
public class RedisLockManager {

    private static Logger logger = LoggerFactory.getLogger(RedisLockManager.class);

    @Autowired
    private JedisPool jedisPool;

    @Around(value = "@annotation(team.lcf.lock.Lock)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {

        Object o = null;
        // 获取注解自定义信息
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Lock annotation = method.getAnnotation(Lock.class);
        String lockType = annotation.LockType();
        Integer timeOut = annotation.timeOut();
        Integer expireTime = annotation.expiteTime();
        // todo 后续引入其他锁实现这里可以采用策略模式
        if (lockType.equals("redis")) {

            Jedis jedis = jedisPool.getResource();
            // 获取所有的lockKey,lockKey采用类名 + 方法名称
            Class<?> aClass = pjp.getTarget().getClass();
            String methodName = pjp.getSignature().getName();
            // 分布式锁key名称
            String lockKey = "lock:" + aClass.getName() + ":" +  methodName;
            // 获取自定义参数,比如类型
            RedisLock redisLock = new RedisLock(jedis, lockKey, expireTime, timeOut);

            try {
                if (redisLock.tryLockBlock()) {
                    try {
                        o = pjp.proceed();
                    } finally {
                        // 释放锁
                        if (redisLock.unLock()) {
                            logger.warn("分布式锁释放失败,key = " + lockKey);
                        }
                    }
                } else {
                    logger.warn("分布式锁获取失败,当前不可用,请稍后再试。 key= " + lockKey );
                }
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                jedis.close();
            }
        } else {
            // 不是redis的暂时不支持
            o = pjp.proceed();
        }

        return o;

    }

}

使用范例:

package team.lcf.service;

import org.springframework.stereotype.Service;
import team.lcf.lock.Lock;
import team.lcf.thread.ThreadUtils;

/**
 * @program: LuoThinking-cloud
 * @description: CzTestService
 * @author: Cheng Zhi
 * @create: 2022-06-14 18:02
 **/
@Service
public class CzTestService {

    @Lock(timeOut = 1000)
    public void print() {
        System.out.println("----开始执行----");
        System.out.println("休息中......");
        ThreadUtils.doSleep(1000L);
        System.out.println("----执行结束----");
    }
}

@GetMapping("/sayHello")
public String sayHello() {

    Object userInfo = ContextHolder.getRequestContext().get("userInfo");
    for (int i=0; i<5; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                service.print();
            }
        }).start();
    }
    return "hello " + userInfo.toString();
}

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第22天,点击查看活动详情

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

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

相关文章

linux中,和,|和||区别

1、& 表示任务在后台执行&#xff0c;比如运行一个jar文件&#xff0c;但是希望是后台运行可以执行 java -jar test.jar & 2、&& 表示前一条命令执行成功时&#xff0c;才执行后一条命令 &#xff0c;如 echo step1‘ && echo ‘step2’ step1执行成功…

Java语法理论和面经杂疑篇《十二. JDK8 - 17新特性》

第18章_JDK8-17新特性&#xff08;下&#xff09; 6. 新语法结构 新的语法结构&#xff0c;为我们勾勒出了 Java 语法进化的一个趋势&#xff0c;将开发者从复杂、繁琐的低层次抽象中逐渐解放出来&#xff0c;以更高层次、更优雅的抽象&#xff0c;既降低代码量&#xff0c;又…

美颜SDK的优化方法:实现高效、快速、精准的美颜处理

时下&#xff0c;美颜相机、美颜软件等美颜处理应用也越来越受到用户的欢迎。美颜SDK是一种为移动应用提供美颜处理能力的开发工具包&#xff0c;可以帮助开发者快速实现美颜功能&#xff0c;提升应用的用户体验。但是&#xff0c;如何实现高效、快速、精准的美颜处理&#xff…

【spring-boot-seckill分布式秒杀系统 v1.0】java秒杀系统源码

spring-boot-seckill分布式秒杀系统是一个用SpringBoot开发的从0到1构建的分布式秒杀系统&#xff0c;项目案例基本成型&#xff0c;逐步完善中。 开发环境&#xff1a; JDK1.8、Maven、Mysql、IntelliJ IDEA、SpringBoot1.5.10、zookeeper3.4.6、kafka_2.11、redis-2.8.4、cur…

设计模式-创建型模式之抽象工厂模式(Abstract Factory)

4.抽象工厂模式(Abstract Factory)4.1. 模式动机在工厂方法模式中具体工厂负责生产具体的产品&#xff0c;每一个具体工厂对应一种具体产品&#xff0c;工厂方法也具有唯一性&#xff0c;一般情况下&#xff0c;一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时…

项目制作-3

今天是好日子呀解决了 文件的问题(未测试) 完成了更多的界面的设计 对基本的模块有了更多的完成度 又学习了easyx图形库的一些的应用 一起来看看吧 void menu() {initgraph(640, 800, 0);IMAGE img;loadimage(&img, "C:\\Users\\唐怡佳\\Desktop\\图1.jpg"…

sql实践-01

CREATE DATABASE 人员管控; SHOW DATABASES; use 人员管控; 创建表 CREATE TABLE 住宿人员 ( 身份证号 CHAR(18) NOT NULL PRIMARY KEY, 姓名 VARCHAR(10) NOT NULL, 性别 CHAR(1) NOT NULL, 出生日期 DATE, 户籍 VARCHAR(30), 文化程度 VARCHAR(10), 婚姻状况 CHAR(…

装上这个插件,你就能一键生成接口文档

当有接口对接需求的时候&#xff0c;开发终于不用再担心 API 文档难写了&#xff0c;也不用再一个个接口文档重新写&#xff01;安装这个 IDEA 插件&#xff0c;可以一步将文档导入到 Postcat。 这款插件操作简单&#xff0c;容易上手&#xff0c;能够让开发者省去API文档编写的…

TypeError: Cannot handle this data type: (1, 1, 33980), |u1

Image.fromarray报错解决 1. 报错信息2. 解决 相关文章 Image.fromarray()详细用法 1. 报错信息 TypeError: Cannot handle this data type: (1, 1, 33980), |u1 2. 解决 报错原因 图片mode格式问题moasic是灰度图片得到的【mode为L】,numpy【值只有0&#xff0c;和255】且…

shell编程入门 第一章 基本语法

shell编程的语法主要分为五个环节&#xff0c;分别是变量&#xff0c;字符串&#xff0c;运算符&#xff0c;流程控制&#xff0c;函数五大部分 shell编程的基础语法 一 变量1.1 shell变量名1.2 使用shell变量1.3只读变量1.4 删除变量 二 字符串2.1 定义时最好用双引号2.2获取字…

EMC 电路设计基础

EMC 在 JIS 中被定义为“电磁兼容性”&#xff0c;意为“不对其他设备产生电磁干扰&#xff0c;即使受到来自其他设备的电磁干扰&#xff0c;仍能保持原有的性能”&#xff0c;也就是具备相互兼顾兼容的性质。 EMC 大致分为 EMI&#xff08;电磁干扰&#xff09;和 EMS&#x…

Chinese-LangChain:基于ChatGLM-6b+langchain实现本地化知识库检索与智能答案生成

Chinese-LangChain Chinese-LangChain&#xff1a;中文langchain项目&#xff0c;基于ChatGLM-6blangchain实现本地化知识库检索与智能答案生成 https://github.com/yanqiangmiffy/Chinese-LangChain 俗称&#xff1a;小必应&#xff0c;Q.Talk&#xff0c;强聊&#xff0c;Qi…

python中的logging模块:从入门到高阶

简单使用 在 Python 中&#xff0c;可以使用内置的 logging 模块来记录应用程序的信息。最简单的用法是使用 basicConfig() 函数配置 logging 模块&#xff0c;然后使用 getLogger() 方法创建一个 Logger 对象&#xff0c;用于记录信息。下面是一个简单的例子&#xff1a; im…

基于html+css的图片展示16

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

No.043<软考>《(高项)备考大全》【第26章】软件工程国家标准

【第26章】软件工程国家标准 1 章节相关1.1 考试相关 2 标准2.1 标准级别及目录 3 软件工程术语4 软件生存周期过程、活动和任务4.1 必背4.2 ★软件生命周期各阶段与软件文档编制工作的关系--熟悉.4.3 各类人员与软件文档的使用关系--熟悉 5 文档控制5.1 SRS需求规格说明书 6 软…

一年一度的PMO大会将于2023年6月在京召开

企业要基业长青就必须持续保持组织活力。企业的内外部环境不会一成不变&#xff0c;顺应变化及时调整变革避免组织出现僵化低效才能在激烈的市场竞争中存活下来。PMO从成立到逐渐发挥越来越强的作用本身即是一种组织变革的过程&#xff0c;从这个意义上来说PMO因组织变革而生&a…

Nginx Rewrite

常用的Nginx正则表达式 ^&#xff1a;匹配输入字符串的起始位置 $ : 匹配输入字符串的结束位置 * : 匹配前面的字符零次或多次 &#xff1a;匹配前面的字符一次或多次 ?: 匹配前面的字符零次或一次 . : 匹配除“\n”之外的任何单个字符 ^$ : 空行 \&#xff1a;将后面…

百度发布Apollo城市智驾,距离AI智能驾驶还有多远?

推荐&#xff1a;将NSDT场景编辑器加入你的3D工具链。 工具集&#xff1a;NSDT简石数字孪生 随着人工智能技术的不断发展&#xff0c;智能驾驶已经成为了汽车行业的一个重要领域。智能驾驶可以减少人为驾驶的错误和疲劳驾驶等不安全因素&#xff0c;提高驾驶安全性&#xff0c…

内蒙农信携手星环科技建设农信大数据平台,激活金融业务创新

背景 内蒙古自治区农村信用社联合社&#xff08;简称“内蒙农信”&#xff09;经过多年的信息化建设&#xff0c;目前投产使用的信息化系统近100套&#xff0c;产生了大量的数据。该社于2019年采用MPP架构的分布式数据库&#xff0c;实现了40多套业务系统数据的归集、标准化处理…

1007、1009:与进制问题、输出问题

1007 题目&#xff1a;本题要求计算A/B&#xff0c;其中A是不超过1000位的正整数&#xff0c;B是1位正整数。你需要输出商数Q和余数R&#xff0c;使得A B * Q R成立 思路&#xff1a;对于数字元素拆分&#xff0c;除法的计算方法用代码实现&#xff08;唯一一点就是在输出的…