互斥锁
需要结合项目中的业务进行回答,通常情况下,分布式锁使用的场景:
集群情况下的定时任务、抢单、幂等性场景
在理解分布式锁之前,我们先来理解简单的单台服务器中的互斥锁的原理。
那么在生产过程中的大多数情况下,同一份代码是会通过nginx的反向代理、负载均衡部署在多个Tomcat中:
单台服务器中的互斥锁仅仅能保证自身的锁对自身线程有效,解决不了不同服务器中的请求加锁问题。这时我们再去使用简单的互斥锁不能去保证抢券时的数据准确性,可能会出现超售。
这时就可以使用分布式锁。可以理解为是在多台服务器部署时的互斥锁!
Redis分布式锁
Redis实现分布式锁主要利用Redis的setnx命令。setnx是SET if not exists(如果不存在,则 SET)的简写。
我们可以注意到,在上图的获取锁的命令中,设置锁和设置超时时间是在同一条命令中定义的,为什么要这样做呢?
获取锁的命令不分开写可以保证添加锁命令的原子性。
定义锁的超时时间可以避免死锁的问题。(获取锁之后突然服务器宕机,锁不能释放,会出现死锁)
那么分布式锁如何合理的控制锁的有效时长呢?
1.根据业务执行时间预估,比较困难。不可控
2.给锁续期,原理:额外增加一个线程,监控锁的持续时间,业务执行时不断增加锁的有效时长。市面上的技术:Redisson,例如:Redisson实现的分布式锁
Redisson实现的分布式锁
什么是Redisson?
Redisson 是一个基于 Redis 的 Java 客户端库,提供了分布式数据结构、分布式锁、分布式集合等功能。Redisson 在 Redis 的基础上封装了更多高级功能,目的是让开发者能够更方便地在 Java 程序中使用 Redis,同时简化开发中的一些复杂问题,如分布式锁、异步任务管理等。
Redisson的原理
底层原理是setnx和lua脚本(保证原子性),后面我们会简要解释什么是lua脚本。
在加锁之后,Redisson看门狗机制会监控业务的执行,不断增加其有效时间,releaseTime默认是30s,每次增加30/3=10s,业务完成后,需要手动释放锁同时看门狗机制解除。
其他线程尝试加锁时会不断重复循环尝试加锁,有一个时间阈值。称为Redisson的尝试等待机制。
代码示例:
在上述代码中,一旦自己定义了锁自动释放时间,那么默认不使用看门狗机制。
加锁、设置过期时间等操作都是基于lua脚本完成的,可以保证命令的原子性
(在 Redis 中执行的 Lua 脚本被视为单个原子操作。整个脚本要么全部执行完毕,要么根本不执行,这确保了脚本内的多条命令以原子方式执行。通过使用 Lua 脚本,开发者可以组合多条 Redis 命令,避免中途被其他命令打断,从而保证执行过程的完整性和一致性。)
可重入
Redis的锁是不可重入的,Redisson的锁是可重入的,可以防止死锁。
重入指某个操作或函数在执行过程中可以再次被调用而不会造成冲突,特别是当同一个线程在进入该操作的过程中,再次尝试进入该操作时,依然能够正常运行。
在Redisson中,重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。
主从一致性
红锁:加锁的节点数量大于所有节点数量的一半,当主节点宕机时可以避免其他线程加到其他的主节点上。
在分布式系统中,如果获取锁的节点数量大于 N/2(即大多数节点),那么即使部分节点宕机或不可用,也能确保加锁行为的有效性。这是因为其他线程或进程在获取锁时,必须在大多数节点上成功加锁才能被视为锁的拥有者。
但是其实现复杂,性能差,运维繁琐,一般不用
Redisson使用Ap思想,看中高可用性,如果一定追求强一致性,建议使用zookeeper(cp思想,看中一致性)
一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)