第八章----Redis分布式锁
1. 什么是分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候便需要使用到分布式锁。
如:多个系统同时在操作某件商品的库存值,如果不能保证互斥性就会造成数据错乱!
分布式锁特性:
互斥(只能有一个客户端获取锁)
不能死锁
容错(只要大部分 redis 节点创建了这把锁就可以)
2. 为什么使用分布式锁
在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(synchronized)进行互斥控制。但随着业务的发展单体项目演变成分布式架构,即多个机器多个线程去完成一件事,这种时候传统的锁机制就已经失效了,需要运用分布式锁来保证数据一致性。
3. 常见的分布式锁有哪些
-
基于MySQL的自身锁机制实现要求数据库支持行级锁,性能差,高并发容易死锁。
-
基于ZooKeeper的节点特性和watch机制,性能比较好,占用资源少,稳定。
-
基于Redis的setnx命令加锁及lua脚本释放锁,性能好,但会占用一部分资源。
4. Redis分布式锁
1. 加锁:当key不存在时给key加上一把值为value的锁,且30s后自动释放锁(具有原子性)。
SET key value NX PX 30000
-
NX:表示只有 key 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 nil)
-
PX 30000:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。(避免死锁)
-
value值防止误删别人的锁,如:A线程执行时间太长锁已经自动释放,而B线程又获取到了锁,如果此时没有value值校验直接删除就会把B线程的锁删掉。
2. 释放锁:就是删除 key ,但是一般可以用 lua 脚本删除,判断 value 一样才删除。
-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
3. StringBoot案例
- 加锁、释放锁
//定义释放锁的lua脚本
private final static DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>(
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end"
, Long.class
);
/**
* 加锁(原子性)
*setnx是『SET if Not eXists』(如果不存在,则 SET)的简写
* @param key 锁的 key 值
* @param requestId 请求id,防止解了不该由自己解的锁 (随机生成)
* @param expireTime 锁的超时时间(秒)
* @param retryTimes 获取锁的重试次数
* @return true 成功 false 失败
*/
public boolean lock(String key, String requestId, long expireTime, int retryTimes) throws InterruptedException {
int count = 0;
while (true) {
if (redisTemplate.opsForValue().setIfAbsent(key, requestId, expireTime, TimeUnit.SECONDS)) {
System.out.println("--jxb--RedisUtil--lock:第"+count+"次加锁成功, key:"+key+", requestId:"+requestId);
return true;
} else {
count++;
System.out.println("--jxb--RedisUtil--lock:第"+count+"次加锁失败, key:"+key+", requestId:"+requestId);
if (retryTimes == count) {
return false;
} else {
Thread.sleep(1000);
continue;
}
}
}
}
/**
* 释放锁(value确保不会误删除)
* @param key
* @param requestId
*/
public void unlock(String key,String requestId){
redisTemplate.execute(UNLOCK_LUA_SCRIPT, Arrays.asList(key), requestId);
}
- 测试,模拟四个客户端并发
private static final String CART = "lock:";
private static final long TIME = System.currentTimeMillis() / 1000;
@Autowired
private RedisUtil redisUtil;
/**
* Description: 修改产品库存
* Author: jxb
* Date: 2020-03-29 15:13:11
*/
@RequestMapping(value = "/updateProduct/{productId}", method = RequestMethod.GET)
public JsonResult<Object> updateProduct(@PathVariable Integer productId) {
// 加锁key
String lockKey = CART + productId;
// requestId, 防止误删别人的锁
String requestId = UUID.randomUUID().toString().replace("-","");
try {
if(redisUtil.lock(lockKey,requestId,30,5)){
// 加锁成功 处理业务逻辑
Thread.sleep(10000);
}else{
// 重试5次,加锁失败抛出异常
return new JsonResult(false, "1001", "服务繁忙,请稍后重试!");
}
}catch (Exception e ){
e.printStackTrace();
}finally {
// 释放锁
redisUtil.unlock(lockKey,requestId);
}
return buildJsonResult(null);
}

4. Redlock 算法
如5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作:
-
得到当前的时间,微秒单位
-
尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间
-
当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
-
如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间
-
如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态
5. Redis VS ZooKeeper 分布式锁的对比
-
redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
-
ZooKeeper 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
你的好运藏在你努力的每一天里