type
status
date
slug
summary
tags
category
icon
password
ext
order
comment
首先,先引入三种锁的概念,分别是线程锁,进程锁,分布式锁
线程锁
这个应该是最熟悉的了,java中Synchronized、ReentrantLock,AQS和Lock的那些都是,保证被锁的方法或代码块在多线程下只有一个线程在访问,当然还分资源独占和资源共享的,像Semaphore、CountdownLatch、CyclicBarrier这种资源共享的一次可以分配多个资源。
进程锁
同一个操作系统内,控制多个进程访问共享资源,因为不同进程是无法干扰对方对资源的访问,所以可以用操作系统的信号量控制。
分布式锁
就是在上面两个的基础上再进行延伸,不同系统,不同进程,不同线程之间的锁,需要通过第三方存储介质来存储锁的信息,常用的Redis,Zookeeper,etcd,MySQL,这几种方式各有优缺,分布式锁是不可能像线程锁那么高效高可用,不那么容易出问题的,所以取决于应用场景,选择适合自己的。
其实他们就是锁的作用范围不同,可以看到 分布式锁>进程锁>线程锁
分布式锁要满足的条件
- 互斥性,任意时刻只有一个客户端可以拿到锁
- 避免死锁,防止一个客户端拿到锁后,长时间不释放,比如客户端崩溃,持锁期间其他代码执行出问题,需要被动的释放,在Redis里使用expire设置过期时间,可以很方便的避免这点
- 一致性,加锁客户端必须和解锁客户端一致,不同客户端不能相互干扰,所以每个客户端申请锁时都要用一个唯一标示的ID作为value,释放锁时对比value相同才允许释放
- 高可用、容错性等等,这篇文章只是单点Redis的,慢慢来。。。。。。
redis分布式锁简易示意图
Redis分布式锁核心指令
SET key value [EX seconds] [PX milliseconds] [NX|XX]
从 Redis 2.6.12 版本开始,
SET
命令的行为可以通过一系列参数来修改:EX seconds
: 将键的过期时间设置为seconds
秒。 执行SET key value EX seconds
的效果等同于执行SETEX key seconds value
。
PX milliseconds
: 将键的过期时间设置为milliseconds
毫秒。 执行SET key value PX milliseconds
的效果等同于执行PSETEX key milliseconds value
。
NX
: 只在键不存在时, 才对键进行设置操作。 执行SET key value NX
的效果等同于执行SETNX key value
。
XX
: 只在键已经存在时, 才对键进行设置操作。
因为
SET
命令可以通过参数来实现 SETNX
、 SETEX
以及 PSETEX
命令的效果, 所以 Redis 将来的版本可能会移除并废弃 SETNX
、 SETEX
和 PSETEX
这三个命令。代码示例
看到这,应该就大概知道了如何利用Redis实现锁,如果set nx成功,说明申请到锁,如果set nx失败,说明key已经存在,被其他进程/线程占用。
如果释放锁,直接删除该key就行,但是为了保证不同线程所删除的是它自己当时申请的锁,防止误删除,需要对比这个value,所以每个线程申请锁时,需要设置一个唯一标识的ID为value,而删除key时,需要对比这个value是否一致,一致才删除,避免特殊情况误删除。
如果正常来想,在程序中首先get key,比较下value,相同则删除,但是这是两步操作:
- get key 并比较value
- 删除key
那么就会出现原子性问题,如果刚get key后且比较value相等,这时候正好其他线程又获取到了锁,那么你删除的就是其他线程的锁,所以需要利用lua脚本,将这两个操作合并为一段代码,在redis中用eval执行lua脚本是具有原子性的,lua脚本如下
如果在redis控制台中执行,那么如下
evel语句的格式: eval script numkeys key [key...] arg [arg...],
其实就是:eval "lua脚本" key数量 key arg arg arg,那个numkeys代表后面的那些N个参数,前几个是key,剩下的则都做为arg
RedisLock实现类
在spring中,直接通过redisTemplate执行redis命令即可,但是要注意redis的序列化器设定的是什么,必须如下。
我刚开始用的还是fastjson作为value的序列化器,转json嘛,无论什么会带上双引号的,用redisTemplate执行lua脚本时,就是不成功,比较value总是不相等,哪怕用string.gsub替换掉双引号再比较也不行,但是手动执行eval命令又可以,所以只能分开,这个redis锁单独用默认序列化器的。
使用方法
因为实现自Lock接口,所以和其他锁使用方法是一样的,测试用例如下
- 作者:Loneking
- 链接:https://loneking.cn/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/96
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。