Redis 笔记二

概览

1.高并发秒杀问题及可能出现的bug

2.秒杀场景JVM级别锁和分布式锁

3.大厂分布式锁Redisson框架

4.从Redisson源码剖析lua解决锁原子性问题

5.从Redisson源码剖析经典锁续命问题

6.Redis主从架构锁失效如何解决

7.Redlock分布式锁高并发下可能存在的问题

8.双十一大促如何将分布式锁性能提升100倍

9.放置订单重复提交或支付分布式锁方案设计

10.防止取消订单误支付bug分布式锁方案设计

1.减库存操作的实现和可能存在的问题

快速待见一个redis环境:Redis和Redis可视化管理工具的下载和安装_redisdesktopmanager下载-CSDN博客

新建一个springboot快速构建:pom.xml

    <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.9</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>18</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.16.3</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

(1)实现一个简单的减库存方案

它有哪些问题?

没有线程安全的保护措施,多个进程访问时,可能会导致超卖问题。

    @Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@RequestMapping("~/deduct_stock")public String deductStock(){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("扣减失败,库存不足");}return "end";}

(2)为了实现线程安全,我们加一个简单的锁Synchronized

同步代码块,通过内置排序锁实现,多个进程访问时,排队进行

它有什么问题?

在单机模式下,能够保证线程安全,但是在分布式集群下,还是会线程不安全,导致超卖问题

    @Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@RequestMapping("~/deduct_stock")public String deductStock(){synchronized (this){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("扣减失败,库存不足");}}return "end";}

(3)使用Redis实现分布式锁

使用分布式锁的原因是在集群结构上加锁,解决集群环境下多并发导致超卖问题。

SETNX命令:  setnx key value

  • 将key的值设置为value,当且仅当key不存在
  • 若key存在,则该命令无任何操作
  • SETNX是Set if not exists的简写
  • 可用版本>=1.0.0

使用该命令保证只有一个用户可以修改成功,另一个用户的操作不生效。

多线程并发在redis排队,单线程处理,拿到锁的处理,锁使用完,要删除,不然会造成死锁问题。

    @Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@RequestMapping("~/deduct_stock")public String deductStock(){String lockKey="product_101";Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zhangsan");//jedis.setnx(k,v)if(!result){return "biz_code";}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("扣减失败,库存不足");}return "end";}

改进一:当程序异常,未及时对持有的锁释放,也会导致死锁问题。

               解决:将锁删除放在finally里,保证一定会执行。

改进二:当程序挂掉,但是锁没有释放,finally也没有执行,还是会导致死锁问题。

               解决:设置超时时间,如果程序挂了,到时间自动释放锁,不影响后续操作。

    @Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@RequestMapping("~/deduct_stock")public String deductStock(){String lockKey="product_101";Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zhangsan");//jedis.setnx(k,v)//设置超时时间stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);if(!result){return "biz_code";}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 {stringRedisTemplate.delete(lockKey);}return "end";}

改进三: 获取锁和超时时间分开写,可能会获取锁还没设置超时时间的时候挂掉,还是会导致前面那个问题。

              解决:用redis内置方法,获取锁的同时设置超时时间,保证操作原子性。

String lockKey="product_101";
//Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zhangsan");//jedis.setnx(k,v)
设置超时时间
//stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zhangsan",10, TimeUnit.SECONDS);//jedis.setnx(k,v)

(4)高并发场景下还有什么问题?

问题一:高并发场景下存在,一个线程刚加锁,就被另一个线程解锁的问题,导致锁一直刚获取就失效。

问题的关键在:不能释放别人的锁。应确保谁加锁,谁释放。

 给锁设置线程ID,确保加锁和释放锁的是同一线程。

Rlock redissonLock=redisson.getLock(lockKey);
redissonLock.lock();
    @Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@RequestMapping("~/deduct_stock")public String deductStock(){String lockKey="product_101";//        Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zhangsan");//jedis.setnx(k,v)
//        //设置超时时间
//        stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);String clientId = UUID.randomUUID().toString();Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10, TimeUnit.SECONDS);//jedis.setnx(k,v)if(!result){return "biz_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("stock")))stringRedisTemplate.delete(lockKey);}return "end";}

问题二:线程获取锁,但是执行过程还没结束,卡顿,但是锁过期了,导致需要重新弄加锁的问题。

解决:无论设置多大的超时时间都有一定的概率导致这个问题,所以解决该问题的核心点是:锁续命机制——线程执行结束前,不断给即将过期的锁增加超时时间,以延长锁的寿命。

finally {if(clientId.equals(stringRedisTemplate.opsForValue().get("stock")))stringRedisTemplate.delete(lockKey);
}

2.Redis分布式加锁与锁续命机制

1.Redisson和Jedis的区别是什么?

Redisson 和 Jedis 的简单比较_redisson代替jedis-CSDN博客

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.6.5</version>
</dependency>
	@Beanpublic Redisson redisson(){//此为单机模式Config config=new Config();config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);//config.setLockWatchdogTimeout(10000);//设置分布式锁watch dog超时时间return (Redisson) Redisson.create(config);}

Controller

@Autowired
private Redisson redisson;

2.Redis加锁的代码

    @Autowiredprivate Redisson redisson;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@RequestMapping("~/deduct_stock")public String deductStock(){String lockKey="product_101";//        Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zhangsan");//jedis.setnx(k,v)
//        //设置超时时间
//        stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);String clientId = UUID.randomUUID().toString();Boolean result=stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10, TimeUnit.SECONDS);//jedis.setnx(k,v)if(!result){return "biz_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 {redissonLock.unlock();
//            if(clientId.equals(stringRedisTemplate.opsForValue().get("stock")))
//                stringRedisTemplate.delete(lockKey);}return "end";}

3.Redis加锁底层实现

加锁的代码:

RLock redissonLock=redisson.getLock(lockKey);
redissonLock.lock();// setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS)

RedissonLock.class的lock()加锁: 

具体的加锁代码是由Lua脚本实现的。

4.Lua脚本实现redis原子性操作

        <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {this.internalLockLeaseTime = unit.toMillis(leaseTime);return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hset', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then" +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"return redis.call('pttl', KEYS[1]);",Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});

Redis Lua脚本

Redis 2.6推出Redis Lua脚本,允许开发者使用Lua语言编写脚本传到Redis中执行。通过内置Lua解释器,可以使用EVAL命令对Lua脚本进行求值。

-------------------------------------------------------------------------------------------------------------------------

Redis Lua的优势

减少网络开销:

        将原先五次请求放入redis服务器上完成。

        使用脚本,减少了网络往返时延,和管道类似。

原子操作:

        Redis将整个脚本作为一个整体执行,不允许中间插入其他命令。管道不是原子的,但Redis的批量操作(类似mset)是原子的

替代Redis事务

        redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规事务功能。官网推荐redis事务可以使用Redis Lua脚本替代。

-------------------------------------------------------------------------------------------------------------------------

EVAL命令格式:

        EVAL script numkeys key [key ...] arg [arg ...]

  • script参数是一段Lua脚本程序,运行再redis内置的lua解释器里。被定义为一个Lua函数。
  • numkeys参数用于指定键名参数个数[key...]
  • 表示脚本中所用到的那些Redis键key。键名可以在Lua通过全局变量KEYS数组,用1为基址的形式访问:KEYS[1]

>eval "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]} 2 key1 key2 first second

)1 "key1"

)2 "key2"

)3 "first"

)4 "second"

return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}   被求值的Lua脚本,数字2指定了键名参数的数量

key1 key2是键名参数,分别使用KEYS[1], KEYS[2]访问

first second是附加参数, 分别用ARGV[1], ARGV[2]访问

-------------------------------------------------------------------------------------------------------------------------

在Lua脚本中,可以使用redis.call()函数执行redis命令

jedis.set("product_stoc_10016","15");//初始化商品10016的库存String script="local count=redis.call('get',KEYS[1])"+"local a=tonumber(count)"+"local b=tonumber(ARGV[1])"+"if a>=b then "+"redis.call('set',KEYS[1],a-b)"+"return 1"+"end"+"return 0";Object obj=jedis.eval(script,Arrays.asList(product_stock_10016),Arrays.asList("10"));
System.out.println(obj);

不要再Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他命令。

redis是单线程、单线程执行脚本。管道不会阻塞redis.

5.Redis可重入锁的实现

为什么可以保证原子性——因为这里直接使用redis的操作命令,redis操作是原子性的。

SETNX可以用作加锁原语(locking primitive)。

如:SETNX lock.foo <current Unix time+lock timeout +1>

返回1:客户端获取锁成功,可通过DEL lock.foo释放锁

返回0:获取失败,有其他客户端加锁

        如果是非阻塞锁(nonblocking lock),则返回调用,或进入一个重试循环,直到获取锁或重试超时。【超时时间是为了解决死锁问题】

Redis在这里加入的锁是可重入锁、异步回调的。

这里断的刷新过期时间,多线程不会死锁吗?这里的锁是可重入锁,异步回调

不断刷新过期时间,过期时间不会无限长吗?

若快过期了但是还没有执行完,则进入锁续命,延续过期时间。

每十秒续命以一次?

RedissonLock.class

3.Redis性能优化

redis单线程处理,相互等待,对于性能还是有影响的

1.主从切换丢锁

数据异步,所以主从切换,如果锁是数据,就会可能由数据丢失导致锁丢失。

场景:主节点数据还没有同步给从节点,主节点挂了,从节点成为新的主节点,但丢失了部分数据。

zookeeper (偏向CP):一致性,锁同步一半才算成功——ZAB崩溃恢复,重新选举的机制,确保数据不丢失

redis (偏向AP):即想要redis高性能,又不想丢锁——redlock:一半加锁即为成功——数据一致性,和zookeeper底层相同——>redlock机制

CAP原理:

【大数据专题】大数据理论基础01之分布式CPA原理深入理解_分布式cpa理论-CSDN博客

百度安全验证

RAFT算法:

RAFT算法详解-腾讯云开发者社区-腾讯云

2.redlock实现原理

Redis使用红锁来解决这个问题:只有当集群中有一半的节点加锁成功,就认为加锁成功。

红锁实现简单,但存在一些问题:

  • 主从切换,切完当前主节点有之前主节点的锁嘛?
  • 切完还能保证一般加锁成功嘛?至少5台,保证最多2台挂掉。
  • 为什么保证奇数台:3-5台奇数台,节约资源,选举无平局。

除此之外,还可以通过持久化来防止数据丢失:

持久化重启,锁丢失,没有一般加锁成功和解——每条命令持久化,但是性能差

通过修改配置打开AOF功能:

# appendonly yes

从现在开始,每执行一个改变数据集的操作,就被追加到AOF文件末尾。

redis重启时,通过重新执行AOF来重建数据集

配置redis多久将数据fsync到磁盘:

appendfsync always:        每次有新命令就追加AOP文件,慢,但安全

appendfsync everysec:        每秒fsync一次追加,快,其丢只丢一秒的数据【推荐,兼顾速度和安全】

appendfsync no:        从不fsync,将数据交给系统处理,更快,但不安全。

3.Redis优化方案

4-5种优化方案:

1.分段锁

将库存数据分成10端,性能提升10倍

2.点好多个页面,后端重复提交|重复支付验证,如何验证

token只能针对同一个页面,多个页面没用来说,不能避免这个问题。可以考虑分布式锁尝试解决。

3.支付和取消同时进行

电商场景可能存在的问题

Redis10大性能优化策略-电子发烧友网

4.redis面试题

Redis系列面试题整理(含参考答案) - 知乎

2021年关于Redis最新的50道面试题(含答案)_以下提供多种 redis 优化的做法,错误的是哪个选项-CSDN博客

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GPT-SoVITS 测试

开箱直用版&#xff08;使用 AutoDL&#xff09; step1 打开地址 https://www.codewithgpu.com/i/RVC-Boss/GPT-SoVITS/GPT-SoVITS-Official 选择 AutoDL创建实例&#xff0c;选择 3080ti 机器 step2 创建好实例之后&#xff0c;进入命令行&#xff0c;输入命令 echo {}>…

Keil导入文件的操作步骤

本文以STM32G431R8T6导入lcd.c文件为例 1 背景 作为最常用的单片机程序编辑工具&#xff0c;全球有超过10万的工程师在使用Keil&#xff0c;但初学者很有可能对Keil的各种信息和操作一无所知&#xff0c;我便是其中一员&#xff0c;由于最近看了很多Keil相关的教程&#xf…

如何从 Android SD 卡恢复已删除的照片

您是否不小心从 Android SD 卡中删除了一些照片&#xff1f;您是否尝试访问昨天拍摄的照片&#xff0c;但无论您在哪里查看都找不到它们&#xff1f;您的 Android 手机的外部存储是否已损坏&#xff0c;其内容无法访问&#xff1f; 在这种情况下&#xff0c;您应该尽快采取行动…

【C++入门到精通】特殊类的设计 |只能在堆 ( 栈 ) 上创建对象的类 |禁止拷贝和继承的类 [ C++入门 ]

阅读导航 引言一、特殊类 --- 不能被拷贝的类1. C98方式&#xff1a;2. C11方式&#xff1a; 二、特殊类 --- 只能在堆上创建对象的类三、特殊类 --- 只能在栈上创建对象的类四、特殊类 --- 不能被继承的类1. C98方式2. C11方法 总结温馨提示 引言 在面向对象编程中&#xff0…

CSS 之 图片九宫格变幻效果

一、简介 ​ 本篇博客用于讲解如何实现图片九宫格变幻的样式效果&#xff0c;将图片分为九块填充在33的的九宫格子元素中&#xff0c;并结合grid、hover、transition等CSS属性&#xff0c;实现元素hover时&#xff0c;九宫格子元素合并为一张完整图片的动画效果。 ​ 为了简化…

小白水平理解面试经典题目LeetCode 594 Longest Harmonious Subsequence(最大和谐字符串)

594 最大和谐字符串 这道题属于字符串类型题目&#xff0c;解决的办法还是有很多的&#xff0c;暴力算法&#xff0c;二分法&#xff0c;双指针等等。 题目描述 和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 1 。 现在&#xff0c;给你一个整数数组 nums …

BAT学习笔记:详解环境变量及其所有创建方法

文章目录 一、初识环境变量二、什么是环境变量三、为什么需要环境变量四、环境变量的分类五、环境变量的设置 一、初识环境变量 1.windows 的搜索框中输入 查看高级系统设置。点击打开系统属性窗口。 2. 在系统属性窗口中&#xff0c;点击右下方的“环境变量”打开环境变量设…

取消Vscode在输入符号时自动补全

取消Vscode在输入符号时自动补全 取消Vscode在输入符号时自动补全问题演示解决方法 取消Vscode在输入符号时自动补全 问题演示 在此状态下输入/会直接自动补全, 如下图 笔者想要达到的效果为可以正常输入/而不进行补全, 如下图 解决方法 在设置->文本编辑器->建议, 取消…

Making Large Language Models Perform Better in Knowledge Graph Completion论文阅读

文章目录 摘要1.问题的提出引出当前研究的不足与问题KGC方法LLM幻觉现象解决方案 2.数据集和模型构建数据集模型方法基线方法任务模型方法基于LLM的KGC的知识前缀适配器知识前缀适配器 与其他结构信息引入方法对比 3.实验结果与分析结果分析&#xff1a;可移植性实验&#xff1…

C++进阶(六)map和set

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、序列式容器与关联式容器二、键值对三、树形结构的关联式容器1、set1、set的介绍2、set的使…

在windows环境下安装hadoop

Hadoop是一个分布式系统基础架构。用户可以在不了解分布式底层细节的情况下&#xff0c;开发分布式程序。但这个架构是基于java语言开发的&#xff0c;所以要先进行jdk的安装&#xff0c;如果电脑已经配置过jdk或者是曾经运行成功过java文件&#xff0c;那就可以跳过第一步。 …

vue3+naiveUI二次封装的v-model 联动输入框

根据官网说明使用 源码 <template><div class"clw-input pt-3"><n-inputref"input":value"modelValue":type"type":title"title"clearable:disabled"disabled":size"size"placeholder&…