谷粒商城-缓存-分布式锁

目录

缓存-分布式锁-分布式锁原理与使用

缓存-分布式锁-Redission简介&整合

缓存-分布式锁-Redission-lock锁测试

缓存-分布式锁-Redission-lock看门狗原理-redission如何解决死锁

缓存-分布式锁-Redission-读写锁测试

缓存-分布式锁-Redission-读写锁补充

缓存-分布式锁-Redission-闭锁测试

缓存-分布式锁-Redission-信号量测试

缓存-分布式锁-缓存一致性解决


缓存-分布式锁-分布式锁原理与使用

我们可以向redis中存储一个map,其中key为lock,value可以是任意值。由这个map充当我们分布式锁,为什么这个map可以充当分布式锁呢?原因是redis的set方法中nx(not exist)参数,如果map已经存在则set失败,如果amp不存在则set成功,这个操作是个原子操作。synchronized(充当锁对象)未获取到锁将继续监听锁是否释放,我们可以休眠一段时间后继续去获取这个锁即自旋。

redis中文文档CRUG网站

 

模拟多线程抢占分布式锁的情形 :

①复制多个连接,使用命令进入redis的客户端

docker exec -it redis redis-cli

 将进入的redis-cli的命令发送给所有连接

 将抢占分布式锁的命令发送给各个redis客户端

 

 查看结果:1号、2号、3号连接全部set失败,只有4号连接set成功

 对三级分类菜单的功能使用分布式锁:

①将synchronized代码段包裹的代码抽取成一个方法

②使用setIfAbsent()方法抢占分布式锁,这个方法实际上就是set nx 或者 setnx

出现问题: 死锁

出现问题的原因:在业务执行过程中出现异常或者服务器宕机则没有执行删除锁的操作,永远无法释放锁,出现死锁问题。

解决方案:设置过期时间,即使没有删除,会自动删除

出现问题: 出现死锁

出现问题的原因:当我们要去设置过期时间时,出现异常或者宕机导致无法设置过期时间

解决方案:抢占锁和设置过期时间必须是原子操作

出现问题: 删除别人正在持有的锁

出现问题的原因:由于我们业务执行时间很长,锁已经过期了,此时删除的可能是别人的锁

解决方案:给锁赋值上uuid,删除锁之前查询是否是同一把锁

出现问题:删除了别正在持有的锁

出现问题的原因:假设我们设置过去时间为10s,业务执行完成花费了9s,查询lock的值花费了0.5s,此时lock还未过期,将lock的值传递到后台花费0.5s,此时lock已经过期但是还是执行了删除锁操作。归根结底,是查询lock的值和删除lock的值需要原子操作

解决方案:使用redis脚本查询lock的值和删除lock

 

脚本: 

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

 DefaultRedisScript的构造器:arg0:脚本 arg1:返回值类型  ; 类的泛型也是返回值类型

存在问题:业务时间如果过长的话需要续费时间

解决方案:将过期时间设置为一个较大的值 

压测一下,看一下会打印几遍查询了数据库

 

缓存-分布式锁-Redission简介&整合

Redission的学习文档地址:Table of Content · redisson/redisson Wiki · GitHub

Redission整合:

①导入Redission的依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.17.1</version>
</dependency>  

 ②配置Redission,采用单例模式

@Configuration
public class MyRedissionConfig {

    @Bean(destroyMethod="shutdown")
     public RedissonClient redisson() throws IOException {
        Config config = new Config();
        // use "rediss://" for SSL connection
        config.useSingleServer().setAddress("redis://192.168.56.22:6379");
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

 ③测试,能打印出来就说明创建成功了

缓存-分布式锁-Redission-lock锁测试

Redission分布式锁和同步器文档地址:8. 分布式锁和同步器 · redisson/redisson Wiki · GitHub

什么是可重入锁? 简而言之,方法的嵌套调用锁的嵌套调用

假设方法A获得1号锁开始执行方法体,在方法A中调用方法B,方法B的也要获得一号锁才可以执行,若一号锁设计为可重入的。方法B可以执行并且执行完成之后方法A释放锁。若一号锁是不可重入的,则方法B一直在等方法A释放锁,会造成死锁问题。因此,锁的设计都应设计为可重入的避免死锁问题。

在hello方法中简单测试lock

RLock实现Lock接口,用法和ReentrantLock一致 。阻塞等待,直到获得锁。

模拟线程抢锁执行业务流程

 线程144获得my-lock锁,执行业务,线程150阻塞等待线程144释放锁。

模拟10000端口服务宕机未执行释放锁操作,看10001端口服务是否能获得锁执行业务,即是否会产生死锁。 

 

从结果可以看出及时释放锁操作没有执行但是锁还是被释放了

Redission的ReetrantLock具有以下特点:

①阻塞等待,默认锁的过期时间为30s

②锁的自动续期,如果业务执行时间过长则会自动给锁续上30s。不用担心业务时间过长导致锁过期

③加锁的业务只要运行完成,就不会给当前锁续期,即使不会手动解锁,锁默认30s以后自动删除

缓存-分布式锁-Redission-lock看门狗原理-redission如何解决死锁

使用lock的设置到期时间自动解锁方法,10s后自动解锁。

测试到期之后是否能自动需期

 出现问题:attempt to unlock lock, not locked by current thread by node id: f132c2f0-3a3d-427a-9cbe-ddaf3aa96a42 thread-id: 140

出现问题的原因:140线程10s之后锁过期了没有自动续期,线程141抢到锁,线程140将线程141设置的锁给解锁了,线程141无锁可解了。

解决方案:自动解锁时间一定要大于业务时间

结论:lock()方法将自动续期,lock(10,TimeUnit.SECONDS)方法将不会自动续期

看门狗自动续期的原理:

如果调用的是lock()方法则传入的leaseTime的时间为-1

tryAcquire()方法实际上就是执行luna脚本创建锁以及设置锁的过期时间

 1是lock(10,TimeUnit.SECONDS)执行luna脚本的方法,2是lock()方法执行luna脚本的方法

lock方法会调用 scheduleExpirationRenewal()方法进行一个续期

 

续期的一个时间是看门狗时间30s 

lock()方法续期最终会调用renewExpiration()方法进行一个续期,在方法中创建一个定时任务task,任务每隔internalLockLeaseTime(看门狗时间:30s) / 3L执行一次续期。

总结:

①lock()方法会自动续期,lock(10,TimeUnit.SECONDS)方法将不会自动续期且自动解锁时间要大于业务时间

②如果我们指定了锁的超时时间,就发送给redis执行脚本,进行占锁,默认时间就是我们设定的实际

③如果我们没有指定锁的超时时间,就是用看门狗的默认时间30s。只要占锁成功就会启动一个定时任务,给锁设置新的过期时间即看门狗时间30s,定时任务每隔10s会自动执行一次续约。

最佳实战:使用lock(30,TimeUnit.SECONDS)

缓存-分布式锁-Redission-读写锁测试

使用tryLock()设置最多等待时间

使用getFairLock()方法获取公平锁,按顺序获得锁,默认是非公平锁靠抢占

并发读的话,使用读锁各个线程互不影响。如果有线程使用写锁,那么必须等写锁释放才能获取读锁。

简单测试读写锁 

写方法

读方法

创建一个writeValue 

 不断刷新不断可以获取值

 调用写方法,读方法需要等待写方法释放锁才能读取,这样做的好处可以读到最新值

写方法一修改完值,读方法可以马上获取到最新的值 

缓存-分布式锁-Redission-读写锁补充

读 + 读 : 相当于无锁,并发读。只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功

写 + 读 : 等待写锁释放

写 + 写  :阻塞等待

读 + 写 :等待读锁释放才能进行写操作

我们测试读 + 写 的模式

 

缓存-分布式锁-Redission-闭锁测试

测试闭锁,模拟场景:放假学校锁门,只有当5个班的人都走光了,门卫才能锁门 

锁门方法一直在等待5个班的人都走了

 

 

缓存-分布式锁-Redission-信号量测试

适用于场景: ①停车位停车 ②分布式限流

 

 

acquire()方法是阻塞方法,当无停车位时将一直等待直到有停车位。 

缓存-分布式锁-缓存一致性解决

使用Redission分布式锁重新编写三级分类获取方法

存在数据一致性问题: 当修改商品的类目时,缓存中的数据与数据库中的数据不一致的问题。

解决方案:

①双写模式:更新完数据库,马上更新缓存

存在问题:读到脏数据

出现问题的原因:线程1执行写数据库操作,将手机改为手机1,由于抢不到CPU,线程1将手机1写入缓存的操作被推迟。此时,线程2抢到CPU,将手机1改为手机2,并且将手机2写入缓存。线程1再将手机1写入缓存。

解决方案:写数据库和写缓存是一个原子操作,加锁。

②失效模式:写数据库,删缓存

存在问题:读到脏数据

出现问题的原因:线程1执行写数据库,将手机修改为手机1并删除了缓存。线程2开始写数据库,将手机1修改为手机2,修改时间较长。线程3读取缓存,发现缓存没有,读取数据库,将读到的手机1写入了缓存中,而此时数据库中的数据为手机2.

解决方案:写数据库和删缓存是一个原子操作,加锁

当我们每次修改数据,数据库的binlog就会记录,Canal就会监听binlog是否被修改,一旦被修改就将其更新至缓存。Canal还能解决为用户推荐其感兴趣商品,将不同的表信息合并成一张表。

 我们系统的解决方案:

①缓存中的所有数据都有过期时间,数据过期下一次查询主动触发更新

②读写数据时,加上分布式的读写锁

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值