何为分布式锁?
分布式应用进行逻辑处理时经常会遇到并发问题,而处理并发问题的方式之一就是分布式锁。在很多场景中,我们为了保证数据的最终一致性,就会选择很多技术方案来支撑,例如分布式事务、分布式锁等,那么什么是分布式锁,分布式锁又会应用到哪些业务场景呢?
业务场景举例
- 一个操作要修改用户的状态,那这个操作又该如何实现分布式锁呢?
那么上述的问题又该如何解决呢?
场景一:修改用户状态,首先分析如何去修改用户的状态,这一操作需要先读出用户的状态,在内存里进行修改,改完了再存回去。但是问题随着就显现出来了,如果这样的操作同时进行,那么就会出现并发问题,因为“读取”
和"保存状态"
这两个操作都不是原子操作
。
原子操作是指不会被线程调度机制打断的操作。这种操作一旦开始,就会一直运行到结束,中间不会有任何线程切换。
分布式锁
分布式锁的本质上要实现的最终目的就是在Redis里面占一个"座位"
,当别的线程也要来占座位时,发现那里已经有一位"同学"
了,就只好选择放弃或者稍后再试。
"占座位"
一般使用setnx(set if not exists)
命令,只允许被一个同学占座位,先来先占,下课了,再调用del
命令释放"座位"
。
127.0.0.1:6379> setnx lock-star 1
(integer) 1
... do something ...
127.0.0.1:6379> del lock-star
(integer) 1
但是问题又来了,如果逻辑执行到中间出现异常了,可能就会导致del
命令没有被调用,这样就会一直卡在这(死锁)
,锁就永远不会得到释放。那么该如何解决呢?我们可以在拿到锁之后,再给锁设置一个过期时间,比如 5s
这样即使中间出现异常也可以保证在5s
后自动将锁释放掉。
127.0.0.1:6379> setnx lock-star 1
(integer) 1
127.0.0.1:6379> expire lock-star 5
... do something ...
127.0.0.1:6379> del lock-star
(integer) 1
给锁加上过期时间,看似解决了,其实逻辑依旧存在漏洞,如果在setnx
和expire
中间服务器进程挂掉了,就会导致expire
无法被调用,也就意味着死锁。
出现这种问题的根源在于setnx
和 expire
是两条指令而不是原子指令
,如果这两条指令可以一起执行就不会出现问题。那么如果使用Redis事务来解决这种问题是否可行呢?
浅提Redis事务过程
Redis提供的事务是将多个命令进行打包,然后一次性的、类似队列先进先出(FIFO)
的特点有序的执行。在执行的过程中不会被打断
,当事务队列中的所有指令都被执行(无论是成功还是失败)之后,事务才会结束!
简单了解完事务的执行过程后会发现,其实在这里运用事务并不可行,因为expire
是依赖于setnx
的执行结果,如果setnx
没有抢到”座位“
,expore
是不应该执行的,这种操作就相当于Java中的if-else
分支逻辑过程,而Redis事务的特点是一气呵成,执行过程中不会被打断,要么全部执行,要么一个都不执行。
如何解决?
为了解决上述问题的出现,在Redis2.8中set
指令新加了扩展参数,使得setnx
和expire
可以一起执行,就可以解决分布式锁的问题。
127.0.0.1:6379> setnx lock-star 1 ex 5 nx
OK
... do something ...
127.0.0.1:6379> del lock-star
(integer) 1
上面这个指令就是setnx
和expire
组合一起的原子指令
。