我们通常使用的synchronized或者Lock都是线程锁,对同一个JVM进程内的多个线程有效。因为锁的本质 是内存中存放一个标记,记录获取锁的线程是谁,这个标记对每个线程都可见。然而我们启动的多个订单服务,就是多个JVM,内存中的锁显然是不共享的,每个JVM进程都有自己的 锁,自然无法保证线程的互斥了,这个时候我们就需要使用到分布式锁了。常用的有三种解决方案:1.基于数据库实现 2.基于zookeeper的临时序列化节点实现 3.redis实现。本文我们介绍的就是redis的实现方式。
  实现分布式锁要满足3点:多进程可见,互斥,可重入。

1) 多进程可见

 redis本身就是基于JVM之外的,因此满足多进程可见的要求。

2) 互斥

 即同一时间只能有一个进程获取锁标记,我们可以通过redis的setnx实现,只有第一次执行的才会成功并返回1,其它情况返回0。

  

 释放锁
 释放锁其实只需要把锁的key删除即可,使用del xxx指令。不过,如果在我们执行del之前,服务突然宕机,那么锁就永远无法删除了。所以我们可以通过setex 命令设置过期时间即可。

import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* 第一种分布式锁
*/
@Component
public class RedisService { private final Logger log = LoggerFactory.getLogger(this.getClass()); @Autowired
JedisPool jedisPool; // 获取锁之前的超时时间(获取锁的等待重试时间)
private long acquireTimeout = 5000;
// 获取锁之后的超时时间(防止死锁)
private int timeOut = 10000; /**
* 获取分布式锁
* @return 锁标识
*/
public boolean getRedisLock(String lockName,String val) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
// 1.计算获取锁的时间
Long endTime = System.currentTimeMillis() + acquireTimeout;
// 2.尝试获取锁
while (System.currentTimeMillis() < endTime) {
// 3. 获取锁成功就设置过期时间
if (jedis.setnx(lockName, val) == 1) {
jedis.expire(lockName, timeOut/1000);
return true;
}
}
} catch (Exception e) {
log.error(e.getMessage());
} finally {
returnResource(jedis);
}
return false;
}
/**
* 释放分布式锁
* @param lockName 锁名称
*/
public void unRedisLock(String lockName) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
// 释放锁
jedis.del(lockName);
} catch (Exception e) {
log.error(e.getMessage());
} finally {
returnResource(jedis);
}
}
// ===============================================
public String get(String key) {
Jedis jedis = null;
String value = null;
try {
jedis = jedisPool.getResource();
value = jedis.get(key);
log.info(value);
} catch (Exception e) {
log.error(e.getMessage());
} finally {
returnResource(jedis);
}
return value;
} public void set(String key, String value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set(key, value);
} catch (Exception e) {
log.error(e.getMessage());
} finally {
returnResource(jedis);
}
}
/**
* 关闭连接
*/
public void returnResource(Jedis jedis) {
try {
if(jedis!=null) jedis.close();
} catch (Exception e) {
}
}
}

上面的分布式锁实现了,但是这时候还可能出现另外2个问题:
 一:获取锁时
  setnx获取锁成功了,还没来得及setex服务就宕机了,由于这种非原子性的操作,死锁又发生了。其实redis提供了 nx 与 ex连用的命令。

  
 二:释放锁时
  1. 3个进程:A和B和C,在执行任务,并争抢锁,此时A获取了锁,并设置自动过期时间为10s
  2. A开始执行业务,因为某种原因,业务阻塞,耗时超过了10秒,此时锁自动释放了
  3. B恰好此时开始尝试获取锁,因为锁已经自动释放,成功获取锁
  4. A此时业务执行完毕,执行释放锁逻辑(删除key),于是B的锁被释放了,而B其实还在执行业务
  5. 此时进程C尝试获取锁,也成功了,因为A把B的锁删除了。
  问题出现了:B和C同时获取了锁,违反了互斥性!如何解决这个问题呢?我们应该在删除锁之前,判断这个锁是否是自己设置的锁,如果不是(例如自己 的锁已经超时释放),那么就不要删除了。所以我们可以在set 锁时,存入当前线程的唯一标识!删除锁前,判断下里面的值是不是与自己标识释放一 致,如果不一致,说明不是自己的锁,就不要删除了。

/**
* 第二种分布式锁
*/
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final Long RELEASE_SUCCESS = 1L; /**
* 尝试获取分布式锁
* @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, "NX", "PX", expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
} /**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
if (jedis.get(lockKey).equals(requestId)) {
System.out.println("释放锁..." + Thread.currentThread().getName() + ",identifierValue:" + requestId);
jedis.del(lockKey);
return true;
}
return false;
}
}

  按照上面方式实现分布式锁之后,就可以轻松解决大部分问题了。网上很多博客也都是这么实现的,但是仍然有些场景是不满足的,例如一个方法获取到锁之后,可能在方法内调这个方法此时就获取不到锁了。这个时候我们就需要把锁改进成可重入式锁了。

  

3) 重入锁

  也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像synchronized就是一个重入锁,它是通过moniter函数记录当前线程信息来实现的。实现可重入锁需要考虑两点:
   获取锁:首先尝试获取锁,如果获取失败,判断这个锁是否是自己的,如果是则允许再次获取, 而且必须记录重复获取锁的次数。
   释放锁:释放锁不能直接删除了,因为锁是可重入的,如果锁进入了多次,在内层直接删除锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。

下面我们假设锁的key为“ lock ”,hashKey是当前线程的id:“ threadId ”,锁自动释放时间假设为20
获取锁的步骤:
1、判断lock是否存在 EXISTS lock
2、不存在,则自己获取锁,记录重入层数为1.
2、存在,说明有人获取锁了,下面判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
3、不存在,说明锁已经有了,且不是自己获取的,锁获取失败.
3、存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20 释放锁的步骤:
1、判断当前线程id作为hashKey是否存在: HEXISTS lock threadId
2、不存在,说明锁已经失效,不用管了
2、存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 ,
  3、获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock

因此,存储在锁中的信息就必须包含:key、线程标识、重入次数。不能再使用简单的key-value结构, 这里推荐使用hash结构。
获取锁的脚本(注释删掉,不然运行报错)

local key = KEYS[]; -- 第1个参数,锁的key
local threadId = ARGV[]; -- 第2个参数,线程唯一标识
local releaseTime = ARGV[]; -- 第3个参数,锁的自动释放时间 if(redis.call('exists', key) == ) then -- 判断锁是否已存在
redis.call('hset', key, threadId, ''); -- 不存在, 则获取锁
redis.call('expire', key, releaseTime); -- 设置有效期
return ; -- 返回结果
end; if(redis.call('hexists', key, threadId) == ) then -- 锁已经存在,判断threadId是否是自己
redis.call('hincrby', key, threadId, ''); -- 如果是自己,则重入次数+1
redis.call('expire', key, releaseTime); -- 设置有效期
return ; -- 返回结果
end;
return ; -- 代码走到这里,说明获取锁的不是自己,获取锁失败

释放锁的脚本(注释删掉,不然运行报错)

local key = KEYS[]; -- 第1个参数,锁的key
local threadId = ARGV[]; -- 第2个参数,线程唯一标识 if (redis.call('HEXISTS', key, threadId) == ) then -- 判断当前锁是否还是被自己持有
return nil; -- 如果已经不是自己,则直接返回
end;
local count = redis.call('HINCRBY', key, threadId, -); -- 是自己的锁,则重入次数-1 if (count == ) then -- 判断是否重入次数是否已经为0
redis.call('DEL', key); -- 等于0说明可以释放锁,直接删除
return nil;
end;

完整代码

import java.util.Collections;
import java.util.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource; /**
* Redis可重入锁
*/
public class RedisLock { private static final StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
private static final DefaultRedisScript<Long> LOCK_SCRIPT;
private static final DefaultRedisScript<Object> UNLOCK_SCRIPT;
static {
// 加载释放锁的脚本
LOCK_SCRIPT = new DefaultRedisScript<>();
LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
LOCK_SCRIPT.setResultType(Long.class); // 加载释放锁的脚本
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
}
/**
* 获取锁
* @param lockName 锁名称
* @param releaseTime 超时时间(单位:秒)
* @return key 解锁标识
*/
public static String tryLock(String lockName,String releaseTime) {
// 存入的线程信息的前缀,防止与其它JVM中线程信息冲突
String key = UUID.randomUUID().toString(); // 执行脚本
Long result = redisTemplate.execute(
LOCK_SCRIPT,
Collections.singletonList(lockName),
key + Thread.currentThread().getId(), releaseTime); // 判断结果
if(result != null && result.intValue() == 1) {
return key;
}else {
return null;
}
}
/**
* 释放锁
* @param lockName 锁名称
* @param key 解锁标识
*/
public static void unlock(String lockName,String key) {
// 执行脚本
redisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(lockName),
key + Thread.currentThread().getId(), null);
}
}

 至此,一个比较完善的redis锁就开发完成了。

3种Redis分布式锁的对比的更多相关文章

  1. 七种方案!探讨Redis分布式锁的正确使用姿势

    前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...

  2. Redisson实现Redis分布式锁的N种姿势(转)

    Redis几种架构 Redis发展到现在,几种常见的部署架构有: 单机模式: 主从模式: 哨兵模式: 集群模式: 我们首先基于这些架构讲解Redisson普通分布式锁实现,需要注意的是,只有充分了解普 ...

  3. Redis 分布式锁|从青铜到钻石的五种演进方案

    缓存系列文章: 缓存实战(一):20 图 |6 千字|缓存实战(上篇) 缓存实战(二):Redis 分布式锁|从青铜到钻石的五种演进方案 缓存实战(三):分布式锁中的王者方案 - Redisson 上 ...

  4. Redis专题(3):锁的基本概念到Redis分布式锁实现

    拓展阅读:Redis闲谈(1):构建知识图谱 Redis专题(2):Redis数据结构底层探秘 近来,分布式的问题被广泛提及,比如分布式事务.分布式框架.ZooKeeper.SpringCloud等等 ...

  5. 论Redis分布式锁的正确使用姿势

    前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...

  6. Redis分布式锁 (图解-秒懂-史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  7. Redis分布式锁

    Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...

  8. redis分布式锁和消息队列

    最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...

  9. redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

随机推荐

  1. LeetCode 141、142环形链表

    141题: 首先,先看141题,这个题是比较初级也是比较经典的环形链表题: 给定一个链表,判断链表中是否有环. 进阶:你能否不使用额外空间解决此题? 那么,什么是有环的链表呢: 这个就是有环的链表 题 ...

  2. x_-admin

    https://www.cnblogs.com/shikaishikai/p/9699076.html

  3. LeetCode 98. 验证二叉搜索树(Validate Binary Search Tree)

    题目描述 给定一个二叉树,判断其是否是一个有效的二叉搜索树. 假设一个二叉搜索树具有如下特征: 节点的左子树只包含小于当前节点的数. 节点的右子树只包含大于当前节点的数. 所有左子树和右子树自身必须也 ...

  4. LeetCode 16. 最接近的三数之和(3Sum Closest)

    题目描述 给定一个包括 n 个整数的数组 nums 和 一个目标值 target.找出 nums 中的三个整数,使得它们的和与 target 最接近.返回这三个数的和.假定每组输入只存在唯一答案. 例 ...

  5. 在一般处理程序中使用session

    public class Handler1 : IHttpHandler, IRequiresSessionState 需要继承 IRequiresSessionState接口,告诉程序要使用sess ...

  6. SQL Server2016 AlwaysOn无域高可用

    https://blog.csdn.net/qq_41981651/article/details/90314817 https://blog.csdn.net/roven257/article/de ...

  7. Mysql的BTREE和HASH索引

    建议默认使用BTree索引,如果时间太长,可以尝试HAST索引,但限制如下: 不支持between and 只支持 = IN <> 不支持范围查询如between and和like. 无法 ...

  8. OpenCV学习笔记(11)——Canny边缘检测

    了解Canny边缘检测的概念 1.原理 Canny边缘检测是一种非常流行的边缘检测算法,是 John F.Canny在1986年提出的.它是一个有很多步构成的算法 1)噪声去除 使用5*5的高斯滤波器 ...

  9. 比较&关系运算符

    <1> 比较(即关系)运算符 python中的比较运算符如下表 运算符 描述 示例 == 检查两个操作数的值是否相等,如果是则条件变为真. 如a=3,b=3则(a == b) 为 true ...

  10. New Date API介绍

    一.新的Date API介绍 LocalDateLocalTimeInstantDurationPeriod formatterparsejdk以前的java.util.Date存在的问题 1)比如n ...