分布式锁——基于Redis分布式锁

单机锁

服务器只有一个,JVM只有一个。
用synchronized加锁,对lock对象加锁,只有线程1结束,线程2,3才会开始。
再用uid避免一个线程多次进来。
在这里插入图片描述

分布式锁

真正上线时:
在这里插入图片描述【注:这些服务器连接的是一个Redis集群(就是同一个Redis)】
在一个JVM1只能锁这个服务器里的lock对象
所以用分布式锁,一个共享的锁。

在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理。

为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行, 为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁应该具备哪些条件:

  • 1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
  • 2、高可用的获取锁与释放锁;
  • 3、高性能的获取锁与释放锁;
  • 4、具备可重入特性;
  • 5、具备锁失效机制,防止死锁;
  • 6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

三种方式:

  • 基于数据库实现分布式锁;(很少)
  • 基于缓存(Redis等)实现分布式锁; (较多)
  • 基于Zookeeper实现分布式锁;

1.基于数据库的实现方式

于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

缺点:

  • 1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能
  • 2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;(没明白)
  • 3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
  • 4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
  • 5、依赖数据库需要一定的资源开销,性能问题需要考虑。

2.基于Redis的实现方式

1、选用Redis实现分布式锁原因:

(1)Redis有很高的性能;
(2)Redis命令对此支持较好,实现起来比较方便

2、使用命令介绍:

(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

(3)delete
delete key:删除key

在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

3、实现思想:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

#连接redis
redis_client = redis.Redis(host="localhost",
                           port=6379,
                           password=password,
                           db=10)

#获取一个锁
lock_name:锁定名称
acquire_time: 客户端等待获取锁的时间
time_out: 锁的超时时间
def acquire_lock(lock_name, acquire_time=10, time_out=10):
    """获取一个分布式锁"""
    identifier = str(uuid.uuid4())
    end = time.time() + acquire_time//当前时间+获取时间=结束时间
    lock = "string:lock:" + lock_name
    while time.time() < end:
        if redis_client.setnx(lock, identifier):#成功设置一个lock_name的锁
            # 给锁设置超时时间, 防止进程崩溃导致其他进程无法获取锁
            redis_client.expire(lock, time_out)#设超时间时间
            return identifier
        elif not redis_client.ttl(lock):#获取键的生存时间
            redis_client.expire(lock, time_out)
        time.sleep(0.001)
    return False

#释放一个锁
def release_lock(lock_name, identifier):
    """通用的锁释放函数"""
    lock = "string:lock:" + lock_name
    pip = redis_client.pipeline(True)
    while True:
        try:
            pip.watch(lock)
            lock_value = redis_client.get(lock)
            if not lock_value:
                return True

            if lock_value.decode() == identifier:
                pip.multi()
                pip.delete(lock)#删除锁
                pip.execute()
                return True
            pip.unwatch()
            break
        except redis.excetions.WacthcError:
            pass
    return False

为什么不用单体锁

单体应用难以满足实际高并发访问需求,会将单体应用部署到多个tomcat实例上,由负载均衡将请求分发到不同实例上。
单体锁(synchronized、ReentrantLock)是JVM层面的锁,只能控制单个实例上的并发访问安全,多实例下依然存在数据一致性问题。

分布式锁

分布式锁指的是,所有服务中的所有线程都去获取同一把锁,但只有一个线程可以成功的获得锁,其他没有获得锁的线程必须全部等待,直到持有锁的线程释放锁。

存在问题:当前线程处理完释放其他线程的锁

问题描述:假设有多个线程,锁的过期时间10s,线程1上锁后执行业务逻辑的时长超过十秒,锁到期释放锁,线程2就可以获得锁执行,此时线程1执行完删除锁,删除的就是线程2持有的锁,线程3又可以获取锁,线程2执行完删除锁,删除的是线程3的锁,如此往后,这样就会出问题。
解决办法就是让线程只能删除自己的锁,即给每个线程上的锁添加唯一标识(UUID实现),删除锁时判断这个标识。

要让删除锁具有原子性,可以利用redis事务或lua脚本实现原子操作判断+删除。(lua脚本的执行是原子的)

//使用Lua脚本实现
    @RequestMapping(" /deduct_stock")
    public String deductStock() {
        String REDIS_LOCK = "good_lock";
        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-","");
        try{
            // 为key加一个过期时间
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);

            // 加锁失败
            if(!flag){
                return "抢锁失败!";
            }
            System.out.println( value+ " 抢锁成功");
            String result = template.opsForValue().get("goods:001");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此处需要调用其他微服务,处理时间较长。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods:001", String.valueOf(realTotal));
                System.out.println("购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8002");
                return "购买商品成功,库存还剩:" + realTotal + "件, 服务端口为8002";
            } else {
                System.out.println("购买商品失败,服务端口为8002");
            }
            return "购买商品失败,服务端口为8002";
        }finally {
            // 谁加的锁,谁才能删除
            // 也可以使用redis事务
            // https://redis.io/commands/set
            // 使用Lua脚本,进行锁的删除

            Jedis jedis = null;
            try{
                jedis = RedisUtils.getJedis();
						//lua脚本命令:
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then " +
                        "return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";

                Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if("1".equals(eval.toString())){
                    System.out.println("-----del redis lock ok....");
                }else{
                    System.out.println("-----del redis lock error ....");
                }
            }catch (Exception e){

            }finally {

                if(null != jedis){
                    jedis.close();
                }
            }
        }
        
    }
}

使用Redisson简化操作

Redis提供的Redisson组件实现Redis锁。
Redisson原理:
在这里插入图片描述
这里UUID+线程ID应该是VALUE, KEY是商品ID
线程去获取锁,锁的VALUE是UUID+线程ID来保证锁和当前形成绑定怒会释放其他线程的锁
当前线程获取锁成功开始处理业务时,内部会有watch dog看门狗,每隔10s看当前线程是否还持有锁,如果持有则给锁延长生存时间。

Redission执行流程如下:(只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下(锁续命周期就是设置的超时时间的三分之一),如果线程还持有锁,就会不断的延长锁key的生存时间。因此,Redis就是使用Redisson解决了锁过期释放,业务没执行完问题。当业务执行完,释放锁后,再关闭守护线程,

RedLock解决Redis集群主从不同步数据丢失问题

Redisson使用主从集群模式,主节点挂掉,从节点没有同步到锁的情况:
使用RedLock,针对Redis中所有节点来进行同步,能够保证超过半数的Redis加锁了才算加锁成功,从而保证并发安全。

用户限流,防止同一用户多次秒杀

使用布隆过滤器记录用户和商品ID来解决。
当用户参与秒杀时,判断是否ID是否记录存在布隆过滤器中,不存在证明该用户是第一次参与秒杀改商品,放行继续后续业务;
过滤器中存在,则禁止继续秒杀。

import cn.hutool.bloomfilter.BitMapBloomFilter;
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
 
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.UUID;
 
@RestController
@CrossOrigin  //开启跨域支持
public class SpikeController {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private BitMapBloomFilter bitMapBloomFilter;
 
    @GetMapping("spike/{userId}/{goodsId}")
    public Object doSpike(@PathVariable Integer userId
            , @PathVariable Integer goodsId, HttpServletResponse response) {
        //设置商品的唯一标识
        String spikeId = userId + "-" + goodsId;
        //判断是否已经参与过
        if (bitMapBloomFilter.contains(spikeId)) {
            return "你已参加过秒杀活动,请选择其他秒杀商品";
        }
        //判断库存
        Long stoke = Long.parseLong(redisTemplate.opsForValue().get(goodsId + "stock"));
        if (stoke < 1) {
            return "该商品已被抢购一空,请选择其他商品";
        }
        //参与过秒杀,添加到过滤器中
        bitMapBloomFilter.add(spikeId);
        //封装发送信息
        Map<String, Integer> spikeMessage = Map.of("userId", userId, "goodsId", goodsId);
        //发送消息
        rabbitTemplate.convertAndSend("", "spike-web", JSON.toJSONString(spikeMessage), message -> {
            MessageProperties messageProperties = message.getMessageProperties();
            String messageId = UUID.randomUUID().toString().replaceAll("-", "");
            messageProperties.setMessageId(messageId);
            return message;
        });
        return "正在拼命抢购中,请稍后查看订单详情...";
    }
}

3.基于ZooKeeper的实现方式

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

FreeRTOS和UCOS操作系统使用笔记

FreeRTOS使用示例 UCOS使用示例 信号量使用 信号量访问共享资源区/ OS_SEMMY_SEM; //定义一个信号量&#xff0c;用于访问共享资源OSSemCreate ((OS_SEM* )&MY_SEM, //创建信号量&#xff0c;指向信号量(CPU_CHAR* )"MY_SEM", //信号量名字(OS_SEM_CTR )1, …

STM32入门笔记(03): ADC(SPL库函数版)(2)

A/D转换的常用技术有逐次逼近式、双积分式、并行式和跟踪比较式等。目前用的较多的是前3种。 A/D转换器的主要技术指标 转换时间 分辨率 例如&#xff0c;8位A/D转换器的数字输出量的变化范围为0&#xff5e;255&#xff0c;当输入电压的满刻度为5V时&#xff0c;数字量每变化…

Google Play上架防关联,打包环境是关联因素之一还是无足轻重?

在Google Play上架应用&#xff0c;对于矩阵式上架或马甲包的开发者来说&#xff0c;防关联的处理技能是必须要精通的。想象一下&#xff0c;你辛辛苦苦开发的应用&#xff0c;因为一些看似微不足道的细节&#xff0c;比如打包环境的问题&#xff0c;就可能被谷歌无情下架或封号…

力扣双指针算法题目:移动零

1.题目 . - 力扣&#xff08;LeetCode&#xff09; 2.思路解析 这个题目的思路和“使用递归排序快速排序解决数组的排序问题”相同 class solution { public:void QuickSort(vector<int>& nums, int left, int right){if (left > right) return;int key left…

信息收集---端口服务信息收集

1. 什么是端口 是设备与外界通讯交流的出口。端口可分为虚拟端口和物理端口&#xff0c;其中虚拟端口指计算机内部或交换机路由器内的端口&#xff0c;不可见。例如计算机中的80端口、21端口、23端口等。物理端口又称为接口&#xff0c;是可见端口&#xff0c;计算机背板的RJ45…

我在高职教STM32——时钟系统与延时控制(2)

大家好&#xff0c;我是老耿&#xff0c;高职青椒一枚&#xff0c;一直从事单片机、嵌入式、物联网等课程的教学。对于高职的学生层次&#xff0c;同行应该都懂的&#xff0c;老师在课堂上教学几乎是没什么成就感的。正因如此&#xff0c;才有了借助 CSDN 平台寻求认同感和成就…

Http 实现请求body体和响应body体的双向压缩方案

目录 一、前言 二、方案一(和http header不进行关联) 二、方案二(和http header进行关联) 三、 客户端支持Accept-Encoding压缩方式,服务器就一定会进行压缩吗? 四、参考 一、前言 有时请求和响应的body体比较大,需要进行压缩,以减少传输的带宽。 二、方案一(和…

助力构建新型电力系统自主可控实时底座

近日&#xff0c;2024亚洲新型电力系统及储能展览会&#xff06;亚洲新型电力及储能论坛会在广州广交会展馆圆满落下帷幕&#xff01;科东软件携多款电力产品亮相展会,并在2024亚洲新型电力及储能论坛发表《“鸿道Intewell操作系统助力构建新型电力系统自主可控实时底座”》的主…

Unity之创建与导出PDF

内容将会持续更新&#xff0c;有错误的地方欢迎指正&#xff0c;谢谢! Unity之创建与导出PDF TechX 坚持将创新的科技带给世界&#xff01; 拥有更好的学习体验 —— 不断努力&#xff0c;不断进步&#xff0c;不断探索 TechX —— 心探索、心进取&#xff01; 助力快速…

2024-2025年本田维修电路图线路图接线图资料更新

此次更新了2024-2025年本田车系电路图资料&#xff0c;覆盖市面上99%车型&#xff0c;包括维修手册、电路图、新车特征、车身钣金维修数据、全车拆装、扭力、发动机大修、发动机正时、保养、电路图、针脚定义、模块传感器、保险丝盒图解对照表位置等等&#xff01; 汽修帮手汽…

日期和时区

日期 时区 修改时区可分为两步 删除系统自带的 localtime 文件 rm -f /etc/localtime 将系统中内置的 Shanghai 文件软连接到 /etc/localtime中 sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

Unity3D中,AI角色Rigidbody旋转导致的动画问题

在制作一些AI角色的时候&#xff0c;可能会运用到Rigidbody组件来使AI角色拥有一些相关的物理属性&#xff0c;但是AI角色在受到一些物理碰撞或者惯性等原因&#xff0c;会发生旋转导致动画出现意料外的错误&#xff0c;比如在由动转静的时候&#xff0c;可能会发生向前翻转等一…

TP8/6 子域名绑定应用

原www.xxx.com/admin改为admincms.xxx.com config/app.php

【Linux】线程——线程的概念、线程的特点、线程的优点和缺点、线程和进程、线程函数的使用

文章目录 Linux线程1. 线程的概念1.1 什么是线程 2. 线程的特点2.1 线程的优点2.2 线程的缺点2.4 线程和进程 3. 线程函数的使用pthread_create() 创建线程pthread_self() 获取线程IDpthread_exit() 线程终止pthread_cancel() 线程取消pthread_join() 线程等待pthread_detach()…

Element-UI - el-table中自定义图片悬浮弹框 - 位置优化

该篇为前一篇“Element-UI - 解决el-table中图片悬浮被遮挡问题”的优化升级部分&#xff0c;解决当图片位于页面底部时&#xff0c;显示不全问题优化。 Vue.directive钩子函数已在上一篇中详细介绍&#xff0c;不清楚的朋友可以翻看上一篇&#xff0c; “Element-UI - 解决el-…

3.js - 色调映射(renderer.toneMapping)

// ts-nocheck// 引入three.js import * as THREE from three// 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls// 导入lil.gui import { GUI } from three/examples/jsm/libs/lil-gui.module.min.js// 导入tween import * as TWEEN…

不同操作系统下的换行符

1. 关键字2. 换行符的比较3. ASCII码4. 修改换行符 4.1. VSCode 5. 参考文档 1. 关键字 CR LF CRLF 换行符 2. 换行符的比较 英文全称英文缩写中文含义转义字符ASCII码值操作系统Carriage ReturnCR回车\r13MacIntosh&#xff08;早期的Mac&#xff09;LinefeedLF换行/新行\…

Windows下载安装配置并使用Redis(保姆级教程)

文章目录 1、Redis的下载与安装 2、Redis的使用 3、Redis的图形界面客户端 4、Redis开机自启动 1、Redis的下载与安装 下载Redis&#xff1a;https://pan.baidu.com/s/1zBonkO2y6AZeqCdRe0W5ow?pwd9999 提取码: 9999 下载后直接解压就可以使用了 2、Redis的使用 我们…

全网最佳硕士研究生复试简历模板

硕士研究生复试简历模板 ✨ 简介 提供了一个适用于国内硕士研究生复试的个人简历模板。该模板通过统一的“样式”形成规范的Word格式&#xff0c;是目前研究生复试的最佳简历模板之一。模板使用“华文中宋”字体&#xff0c;如您的电脑中未安装此字体&#xff0c;请提前安装。…

软件测试与质量保证 | 云班课选择题库

目录 第1章课后习题 第2章课后习题 第3章课后习题 第4章课后习题 第5章课后习题 第6章课后习题 第7章课后习题 第8章课后习题 第9章课后习题 第10章课后习题 第11章课后习题 第12章课后习题 第13章 测试相关未分类习题 第1章课后习题 1. 与质量相关的概念包括 &a…