并发解决方案之Redis实现分布式锁

分布式锁

- 为什么要使用分布式锁?
因为存在并发产生的数据安全问题。

在生产环境中,通常会出现并发的场景,多个用户请求同一个接口时,若接口不对并发进行处理,则会造成数据的不安全,传统单体架构的应用我们可以使用synchronized锁或者是lock锁进行解决,但是在现在,多数应用都是集群部署,使用synchronized只是在单应用层面对JVM进行加锁,对集群部署的应用几乎没用,所以需要使用分布式锁。
我的理解是,高并发实际上就是多个线程在争抢唯一的一块资源,需要使用锁来证明是哪个线程拿到了这块资源,谁拿到这块资源,谁就可以执行自己的业务逻辑代码,证明谁拿到这块资源需要将锁放在所有线程都能看到的地方(存放在Redis),所以使用synchronized就力不从心,使用synchronized相当于是在自己的JVM内锁住,可是相同的方法不值存在于一台JVM的方法栈中。
若以下代码是单体架构部署,则不会出现线程安全问题,若是集群部署,则会出现线程安全问题。
在这里插入图片描述

- 测试可能存在并发问题的接口。

使用Jmeter压测接口,可以发现上图代码在单体架构线程安全,可是在集群部署很容易出现线程间数据不安全的问题
如何使用Jmeter:我还没写(回头写,先挖个坑(逃))

- 如何解决上图代码集群部署线程不安全问题?

:使用分布式锁。
:如何实现分布式锁?
:借助Redis单线程以及setnx的特性实现分布式锁。
:什么是setnx?
setnx
---setnx key value
---setnx Grover 666 执行成功
---setnx Grover 777 执行失败(因为Grover的key已存在)

对上图线程不安全代码进行改进(入门级别分布式锁)

加锁Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,v. "zhuge");
释放锁stringRedisTemplate.delete(lockkey);
在这里插入图片描述
:上述修改后的入门分布式锁有没有什么问题?
:死锁问题:当程序拿到锁在执行业务逻辑时,可能会出现抛异常的情况,这个时候程序代码就不能执行到释放锁的代码,此时别的线程也拿不到锁,就出现了死锁问题。
:那抛异常可以使用try-catch-finally解决,将释放锁的代码放在finally代码块中。
:那要是服务宕机呢?
:我可以给锁的key设置一个过期时间,只要超过过期时间自动释放锁。
像这样:
在这里插入图片描述
:那要是第一行执行成功后就宕机了呢?
:那就保证这个操作的原子性(不可分割的最小部分),这样写:
原子性加锁
:这个代码还存在什么问题吗?
:存在锁永久失效的可能。图解:线程一执行业务逻辑到了10s,线程一的锁被释放,此时线程二拿到锁,线程一继续执行剩下的5s的业务代码,执行完毕后该释放锁,此时锁已经是线程二的锁了,线程一释放了线程二的锁,以此类推 ,相当于锁的永久失效。
在这里插入图片描述
:如何解决锁永久失效的问题?
:现在的问题是线程一释放掉线程二的锁,线程二释放线程三的锁,只要保证当前线程只能释放自己的锁就好。
代码如下:给每个线程一个clientID,没次释放锁的时候通过clientId判断一下当前释放的锁锁是不是当前线程的锁。
在这里插入图片描述
:有的公司确实这样实现分布式锁屏,可是当前代码是否还存在什么问题?
:有,还是会可能出现锁失效的可能,假如线程一花费了9.9s已经执行完finally中的if判断,正准备去释放锁,此时出现了系统卡顿的情况 ,释放锁的代码并没有被执行,可是10s已经到了(系统卡顿代码未执行与redis无关,redis该到期就到期),此时线程一释放的就是线程二的锁了。
:你说的还是释放锁的代码原子性的问题,如何解决呢?
:我们先不探讨代码原子性问题,先试着可以将锁超时时间变得久一点看行不行。
:那也无法根本解决问题,业务逻辑的执行时间仍然不确定,而且锁的时间越久,出现问题后用户等待的时间也就越久,所以锁不宜设置的太久,设置的时间太久也没有意义,有没有什么其他方案保证线程安全呢?
:可以使用锁续期的解决方案,锁续期就是除了主线程之外,每条线程再配备一个B线程,B线程定时去判断当前主线程的锁是否还存在,若存在,就将锁进行续期,若不存在,就说明锁已经被释放,不需要续期。需要注意的是,锁的失效时间大于B线程定时任务判断锁是否存在的任务时间,若锁失效时间是10s,定时任务时间要小于10s.
:可是我给代码加定时任务,加超时时间,加那么多的控制线程安全的代码,我怕写错或者考虑欠缺,总是没有安全感,怎么办呢?
:可以使用Redisson.Redisson-Github
:什么是Redisson呢?
:可以理解为Java操作Redis的客户端,和Jedis性质一样。Redisson特别善于解决分布式问题。

- 如何使用Redisson?
  1. 在项目中加入Redisson的依赖
    Redisson依赖
  2. 配置Redis,创建Bean
    创建Bean
    此处创建的是Redis单机模式(只有一台Redis),在生产环境中有很多模式,如下图的哨兵模式,主从模式,集群等等。
    在这里插入图片描述
  3. 使用Redisson解决高并发问题(三行代码搞定)
    在这里插入图片描述
- Redisson底层 .lock()实现原理

在这里插入图片描述

redisson.lock();最核心的加锁代码如下:
在这里插入图片描述
:上面带那种script是什么东西呀?
:是Lua脚本
:什么是Lua脚本?
:Lua脚本就是一段运行在服务端的脚本语言,可以被多个语言调用,Redis2.6之后推出了Lua脚本功能,也就是说,可以在Redis中运行一段Lua脚本,而在Redis中运行Lua脚本的好处有以下几点。
Redis中使用Lua脚本
:那上面那段Lua脚本是什么意思呢?
:上面那段Lua代表,设置一个hset key value;
key为当前锁的名称(redisson.getLock(锁名称));
value为当前主线程ID。
并设置一个默认过期时间30s;
:那锁续期在哪里实现呢?
:在以下定时任务中这段Lua脚本中实现,判断当前线程锁是否存在,若存在,则续期。(这个定时任务循环调用自己,并且是延时执行,相当于是30/3=10s秒钟执行一次)在这里插入图片描述

以下代码还存在其他问题吗?

在这里插入图片描述
大多数互联网公司使用Redisson直接这样使用就可以解决并发问题,还需要注意的就是redis的集群问题,因为多数公司的redis都不止一台。

还存在哪些问题?

问题一:当线程一使用Redis的主节点加锁成功后,线程一继续执行自己的业务代码,这时,Redis的主节点挂了,由于Redis主从复制是异步操作,加锁信息还没有同步到子节点,这时候线程二访问Redis,发现没有加锁信息,于是线程二也开始执行业务代码,就出现了Redis在主从切换时,key丢失导致锁失效的问题

如何解决Redis在主从切换时,key丢失导致锁失效的问题?
使用zk替换redis作为分布式锁

  • 首先要知道的是:Redis遵循CAP原则中的AP原则-可用性(Availability)、分区容错性(Partition tolerance),在加锁成功之后,redis直接将加锁成功的信息返回给业务逻辑主线程,这时候业务逻辑线程开始执行。
  • 再了解一下Zookeeper的机制:Zookeeper遵循CAP中的CP-一致性(Consistency)、分区容错性(Partition tolerance),他的机制是在加锁成功后,先将加锁信息传递给半数以上子节点后,再将加锁成功的信息返回给业务逻辑主线程,这时候业务逻辑线程开始执行。若zk的leader节点挂了之后,zk中的选举机制会保证同步成功的子节点变为leader节点。而且zk有主节点才允许被写入key。使用Zookeeper就不会出现主从切换key丢失的问题。

使用Redlock红锁解决不推荐,红锁中还存在很多小问题)

  • 每次加锁的时候也给半数以上节点加锁,才能算是加锁成功,才能继续执行业务逻辑代码。在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

问题二性能问题,分布式锁其实就是将原本应该并行执行的场景进行串行化,只能等待前一个线程执行完毕释放锁之后,之后等待的线程才有可能执行。

如何解决加锁后的性能问题?

  • 将加锁的粒度降低:将加锁代码的范围减少,只对处理可能产生并发的代码段进行加锁。
  • 使用分段锁的机制提高锁的性能(ConcurrentHashMap中使用的也是分段锁)
  • 分段锁:
    现在要对Product_101的商品进行秒杀100份
    我们将Product_101分为若干个小份
    Product_101_0 20份
    Product_101_1 20份
    Product_101_2 20份
    … … . … … … .20份
    此时就有5个线程可以并发执行,以Product_101_n为加锁的key.
    在高并发场景下,例如12306抢票或者双11秒杀,一定有redis集群,我们还可以将Product_101_n通过某些方式放在不同的redis上提高性能。

完结撒花~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1.redis支持的数据结构 string list hash set zset(基本回答) 加分项:另外redis还对这几种数据结构做了扩展,如GEO对位置计算,hyperLogLog做统计,bitmaps:redis底层存储value值都是存储的二进制数据,redis提供bitmaps(位图)可以直接访问或修改底层存储的二进制数据 2.redis线程模型 redis是单线程实现。 3.redis 提供的持久机制 redis 支持rdb和aof两种持久机制,redis4.0后支持混合持久化。rdb是定时的持久机制,宕机有可能会丢失最后一次持久化之后存在数据丢失。aof是基于操作日志追加的持久机制。(基本回答) 加分项: 1.rdb持久化原理 原理是redis会单独创建(fork)一个与当前进程一模一样的子进程来进行持久化, 这个子线程的所有数据(变量。环境变量,程序程序计数器等)都和原进程一模一样,会先将数据写入到一个临时文件中, 待持久化结束了,再用这个临时文件替换上次持久化好的文件 2.他什么时候fork子进程,或者什么时候触发rdb持久化机制 shutdown时,如果没有开启aof,会触发 配置文件中默认的快照配置 执行命令save或者bgsave save是只管保存,其他不管,全部阻塞 bgsave: redis会在后台异步进行快照操作,同时可以响应客户端的请求,但是在调用fork函数时是阻塞的,很快,可以忽略不计 执行flushall命令 但是里面是空的,无意义 3.aof原理? 原理是将Reids的操作日志以追加的方式写入文件,读操作是不记录的 2.触发机制(根据配置文件配置项) no:表示等操作系统进行数据缓存同步到磁盘(快,持久化没保证) always:同步持久化,每次发生数据变更时,立即记录到磁盘(慢,安全) everysec:表示每秒同步一次(默认值,很快,但可能会丢失一秒以内的数据) 以下问题都是基本回答: 4.redis支持事务吗 redis可以说是半支持事务(假事务),提供了一些在一定程度上支持线程安全和事务的命令。例如:multi/exec watch inc等。但是redis的事务并不支持回滚,即可以两个命令可以同时提交执行,但是如果有失败,成功的也不会回滚 5.你们公司使用的是什么集群模式 看你写的项目经验,如果你们公司数据量特别大,公司用缓存牛逼,就说Rediscluster,小公司可以说哨兵集群 1.哨兵模式 基本回答:哨兵主要就是启动哨兵(redis特殊)节点,对主节点进行监控,如果半数以上发现ping主节点不通了,认为主节点挂了,则进行故障转移,就是选出一个从节点代替主节点 2.Rediscluster集群模式 基本回答:Rediscluster是一个高可用集群,它基于分片(对key进行crc16,然后对16384取余)的原理,可以把他理解为是由多组哨兵集群组成,但是它不依赖哨兵 6.缓存穿透 缓存穿透指的是使用不存在的key进行大量的高并发查询,这导致缓存无法命中,每次请求都要穿透到后端数据库系统进行查询,数据库压力过大。 常用解决方案:将空值缓存起来。 其他解决方案:使用布隆过滤器(guava 19开始已支持布隆过滤器) 备注:如果你可以理解太白老师讲的基于redis位图自己实现的布隆过滤器,可以说说,更加分 7.缓存击穿 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力 解决方案:1.互斥 如果项目不会多部署则可以使用jvm,如果会多部署则使用分布式锁 8.缓存雪崩 缓存雪崩指缓存服务器重启或者大量缓存集中在某一个时间段内失效 常用解决办法: 1.主要就是要搭建高可用集群,保证机器的高可用。 2.对不同的数据使用不同的失效时间,甚至对相同的数据、不同的请求使用不同的失效时间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

难打的仗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值