在很多场景中, 我们为了保证数据的最终一致性, 需要很多的技术方案来支持, 比如分布式事务, 分布式锁等. 那具体什么是分布式锁, 分布式锁应用在哪些业务场景, 如何来实现分布式锁呢?
0.1 什么是分布式锁
要介绍分布式锁, 首先要提到与分布式锁相对应的是线程锁, 进程锁.
1. 线程锁
主要用来给方法, 代码块加锁. 当某个方法或代码使用锁, 在同一时刻仅有一个线程执行该方法或该代码段. 线程锁只在同一 JVM 中有效果, 因为线程锁的实现在根本上是依靠线程之间共享内存实现的, 比如 synchronized 是共享对象头, 显示锁 Lock 是共享某个变量(state).
2. 进程锁
为了控制同一操作系统中多个进程访问某个共享资源, 因为进程具有独立性, 各个进程无法访问其他进程的资源, 因此无法通过 synchronized 等线程锁实现进程锁.
3. 分布式锁
当多个进程不在同一个系统中, 用分布式锁控制多个进程对资源的访问.
0.2 分布式锁的要求
首先, 为了确保分布式锁可用, 我们至少要确保锁的实现同时满足以下四个条件:
互斥性: 任意时刻, 只能有一个客户端获取锁, 不能同时有两个客户端获取到锁.
安全性: 锁只能被持有该锁的客户端删除, 不能由其它客户端删除.
死锁: 获取锁的客户端因为某些原因 (如 down 机等) 而未能释放锁, 其它客户端再也无法获取到该锁.
容错: 当部分节点(Redis 节点等)down 机时, 客户端仍然能够获取锁和释放锁.
0.3 分布式锁的应用场景
在传统单体应用单机部署的情况下, 可以使用 Java 并发处理相关的 API(如 ReentrantLcok 或 synchronized)进行互斥控制.
但是在分布式系统后, 由于分布式系统多线程, 多进程并且分布在不同机器上, 这将使原单机部署情况下的并发控制锁策略失效, 为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问, 这就是分布式锁的来源.
分布式锁应用场景大都是用在高并发, 大流量场景. 当多个进程不在同一个系统中, 就需要用分布式锁控制多个进程对资源的访问.
0.4 分布式锁的实现
分布式锁一般有三种实现方式:
数据库乐观锁;
基于 ZooKeeper 的分布式锁;
基于 Redis 的分布式锁;
0.5 Redis 实现分布式锁
基于 Redis 命令: SET key value NX EX max-lock-time
这里补充下: 从 2.6.12 版本后, 就可以使用 set 来获取锁, Lua 脚本来释放锁. setnx 是老黄历了, set 命令 nx,xx 等参数, 是为了实现 setnx 的功能.
1. 加锁
- public class RedisTool {
- private static final String LOCK_SUCCESS = "OK";
- private static final String SET_IF_NOT_EXIST = "NX";
- private static final String SET_WITH_EXPIRE_TIME = "PX";
- /**
- * 尝试获取分布式锁
- * @param jedis Redis 客户端
- * @param lockKey 锁
- * @param requestId 请求标识
- * @param expireTime 超期时间
- * @return 是否获取成功
- */
- public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
- String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
- if (LOCK_SUCCESS.equals(result)) {
- return true;}return false;}
- }
- jedis.set(String key, String value, String nxxx, String expx, int time)
这个 set()方法一共有五个形参:
第一个为 key, 我们使用 key 来当锁, 因为 key 是唯一的.
第二个为 value, 我们传的是 requestId, 很多童鞋可能不明白, 有 key 作为锁不就够了吗, 为什么还要用到 value? 原因就是我们在上面讲到可靠性时, 分布式锁要满足第四个条件解铃还须系铃人, 通过给 value 赋值为 requestId, 我们就知道这把锁是哪个请求加的了, 在解锁的时候就可以有依据. requestId 可以使用 UUID.randomUUID().toString()方法生成.
第三个为 nxxx, 这个参数我们填的是 NX, 意思是 SET IF NOT EXIST, 即当 key 不存在时, 我们进行 set 操作; 若 key 已经存在, 则不做任何操作;
第四个为 expx, 这个参数我们传的是 PX, 意思是我们要给这个 key 加一个过期的设置, 具体时间由第五个参数决定.
第五个为 time, 与第四个参数相呼应, 代表 key 的过期时间.
2. 解锁
- public class RedisTool {
- private static final Long RELEASE_SUCCESS = 1L;
- /**
- * 释放分布式锁
- * @param jedis Redis 客户端
- * @param lockKey 锁
- * @param requestId 请求标识
- * @return 是否释放成功
- */
- public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
- String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
- Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
- if (RELEASE_SUCCESS.equals(result)) {
- return true;
- }return false;
- }
- }
那么这段 Lua 代码的功能是什么呢? 其实很简单, 首先获取锁对应的 value 值, 检查是否与 requestId 相等, 如果相等则删除锁(解锁). 那么为什么要使用 Lua 语言来实现呢, 留给大家讨论探索.
0.6 其他分布式锁实现方案
还可以使用 Redission(Redis 的客户端)集成实现分布式锁, 也可以使用数据库, zookeeper 等来实现
---END---
如果觉得不错, 记得点赞, 关注, 之后小编会不断更新~
来源: http://www.jianshu.com/p/470ed66be8d9