分布式锁是在分布式环境下(多个JVM进程)控制多个客户端对某一资源的同步访问的一种实现,与之相对应的是线程锁,线程锁控制的是同一个JVM进程内多个线程之间的同步。分布式锁的一般实现方法是在应用服务器之外通过一个共享的存储服务器存储锁资源,同一时刻只有一个客户端能占有锁资源来完成。通常有基于Zookeeper,Redis,或数据库三种实现形式。本文介绍基于Redis的实现方案。

要求

基于Redis实现分布式锁需要满足如下几点要求:

  1. 在分布式集群中,被分布式锁控制的方法或代码段同一时刻只能被一个客户端上面的一个线程执行,也就是互斥
  2. 锁信息需要设置过期时间,避免一个线程长期占有(比如在做解锁操作前异常退出)而导致死锁
  3. 加锁与解锁必须一致,谁加的锁,就由谁来解(或过期超时),一个客户端不能解开另一个客户端加的锁
  4. 加锁与解锁的过程必须保证原子性

实现

1. 加锁实现

基于Redis的分布式锁加锁操作一般使用 SETNX 命令,其含义是“将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作”。

在 Spring Boot 中,可以使用 StringRedisTemplate 来实现,如下,一行代码即可实现加锁过程。(下列代码给出两种调用形式——立即返回加锁结果与给定超时时间获取加锁结果)

/**
* 尝试获取锁(立即返回)
* @param key 锁的redis key
* @param value 锁的value
* @param expire 过期时间/秒
* @return 是否获取成功
*/
public boolean lock(String key, String value, long expire) {
return stringRedisTemplate.opsForValue().setIfAbsent(key, value, expire, TimeUnit.SECONDS);
} /**
* 尝试获取锁,并至多等待timeout时长
*
* @param key 锁的redis key
* @param value 锁的value
* @param expire 过期时间/秒
* @param timeout 超时时长
* @param unit 时间单位
* @return 是否获取成功
*/
public boolean lock(String key, String value, long expire, long timeout, TimeUnit unit) {
long waitMillis = unit.toMillis(timeout);
long waitAlready = 0; while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, expire, TimeUnit.SECONDS) && waitAlready < waitMillis) {
try {
Thread.sleep(waitMillisPer);
} catch (InterruptedException e) {
log.error("Interrupted when trying to get a lock. key: {}", key, e);
}
waitAlready += waitMillisPer;
} if (waitAlready < waitMillis) {
return true;
}
log.warn("<====== lock {} failed after waiting for {} ms", key, waitAlready);
return false;
}

上述实现如何满足前面提到的几点要求:

  1. 客户端互斥: 可以将expire过期时间设置为大于同步代码的执行时间,比如同步代码块执行时间为1s,则可将expire设置为3s或5s。避免同步代码执行过程中expire时间到,其它客户端又可以获取锁执行同步代码块。
  2. 通过设置过期时间expire来避免某个客户端长期占有锁。
  3. 通过value来控制谁加的锁,由谁解的逻辑,比如可以使用requestId作为value,requestId唯一标记一次请求。
  4. setIfAbsent方法 底层通过调用 Redis 的 SETNX 命令,操作具备原子性。

错误示例:

网上有如下实现,

public boolean lock(String key, String value, long expire) {
boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
if(result) {
stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return result;
}

该实现的问题是如果在result为true,但还没成功设置expire时,程序异常退出了,将导致该锁一直被占用而导致死锁,不满足第二点要求。

2. 解锁实现

解锁也需要满足前面所述的四个要求,实现代码如下:

private static final String RELEASE_LOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private static final Long RELEASE_LOCK_SUCCESS_RESULT = 1L;
/**
* 释放锁
* @param key 锁的redis key
* @param value 锁的value
*/
public boolean unLock(String key, String value) {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT, Long.class);
long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value);
return Objects.equals(result, RELEASE_LOCK_SUCCESS_RESULT);
}

这段实现使用一个Lua脚本来实现解锁操作,保证操作的原子性。传入的value值需与该线程加锁时的value一致,可以使用requestId(具体实现下面给出)。

错误示例:

   public boolean unLock(String key, String value) {
String oldValue = stringRedisTemplate.opsForValue().get(key);
if(value.equals(oldValue)) {
stringRedisTemplate.delete(key);
}
}

该实现先获取锁的当前值,判断两值相等则删除。考虑一种极端情况,如果在判断为true时,刚好该锁过期时间到,另一个客户端加锁成功,则接下来的delete将不管三七二十一将别人加的锁直接删掉了,不满足第三点要求。该示例主要是因为没有保证解锁操作的原子性导致。

3. 注解支持

为了方便使用,添加一个注解,可以放于方法上控制方法在分布式环境中的同步执行。

/**
* 标注在方法上的分布式锁注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLockable {
String key();
String prefix() default "disLock:";
long expire() default 10L; // 默认10s过期
}

添加一个切面来解析注解的处理,

/**
* 分布式锁注解处理切面
*/
@Aspect
@Slf4j
public class DistributedLockAspect { private DistributedLock lock; public DistributedLockAspect(DistributedLock lock) {
this.lock = lock;
} /**
* 在方法上执行同步锁
*/
@Around(value = "@annotation(lockable)")
public Object distLock(ProceedingJoinPoint point, DistributedLockable lockable) throws Throwable {
boolean locked = false;
String key = lockable.prefix() + lockable.key();
try {
locked = lock.lock(key, WebUtil.getRequestId(), lockable.expire());
if(locked) {
return point.proceed();
} else {
log.info("Did not get a lock for key {}", key);
return null;
}
} catch (Exception e) {
throw e;
} finally {
if(locked) {
if(!lock.unLock(key, WebUtil.getRequestId())){
log.warn("Unlock {} failed, maybe locked by another client already. ", lockable.key());
}
}
}
}
}

RequestId 的实现如下,通过注册一个Filter,在请求开始时生成一个uuid存于ThreadLocal中,在请求返回时清除。

public class WebUtil {

    public static final String REQ_ID_HEADER = "Req-Id";

    private static final ThreadLocal<String> reqIdThreadLocal = new ThreadLocal<>();

    public static void setRequestId(String requestId) {
reqIdThreadLocal.set(requestId);
} public static String getRequestId(){
String requestId = reqIdThreadLocal.get();
if(requestId == null) {
requestId = ObjectId.next();
reqIdThreadLocal.set(requestId);
}
return requestId;
} public static void removeRequestId() {
reqIdThreadLocal.remove();
}
} public class RequestIdFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String reqId = httpServletRequest.getHeader(WebUtil.REQ_ID_HEADER);
//没有则生成一个
if (StringUtils.isEmpty(reqId)) {
reqId = ObjectId.next();
}
WebUtil.setRequestId(reqId);
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
WebUtil.removeRequestId();
}
}
} //在配置类中注册Filter /**
* 添加RequestId
* @return
*/
@Bean
public FilterRegistrationBean requestIdFilter() {
RequestIdFilter reqestIdFilter = new RequestIdFilter();
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(reqestIdFilter);
List<String> urlPatterns = Collections.singletonList("/*");
registrationBean.setUrlPatterns(urlPatterns);
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return registrationBean;
}

4. 使用注解

@DistributedLockable(key = "test", expire = 10)
public void test(){
System.out.println("线程-"+Thread.currentThread().getName()+"开始执行..." + LocalDateTime.now());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程-"+Thread.currentThread().getName()+"结束执行..." + LocalDateTime.now());
}

总结

本文给出了基于Redis的分布式锁的实现方案与常见的错误示例。要保障分布式锁的正确运行,需满足本文所提的四个要求,尤其注意保证加锁解锁操作的原子性,设置过期时间,及对同一个锁的加锁解锁线程一致。原文地址: http://blog.jboost.cn/distributedlock.html


[转载请注明出处]

作者:雨歌

欢迎关注作者公众号:半路雨歌,查看更多技术干货文章

基于Redis分布式锁的正确打开方式的更多相关文章

  1. Redis 分布式锁的正确打开方式

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

  2. Redis分布式锁的正确实现方式

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

  3. Redis(十三):Redis分布式锁的正确实现方式

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

  4. Redis分布式锁的正确实现方式(Java版)

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

  5. Redis 分布式锁的正确实现方式(转)

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

  6. 【转】Redis 分布式锁的正确实现方式( Java 版 )

    链接:wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/ 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布 ...

  7. Redis 分布式锁的正确实现方式(Java版)[转]

    本文来源: https://www.cnblogs.com/linjiqin/p/8003838.html 前言 分布式锁一般有三种实现方式: 数据库乐观锁: 基于Redis的分布式锁: 基于ZooK ...

  8. Redis分布式锁【正确实现方式】

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

  9. redis 分布式锁的正确实现方式

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

随机推荐

  1. cb24a_c++_STL算法简介

    算法概述 算法部分主要由头文件<algorithm>,<numeric>和<functional>组成.       <algorithm>是所有STL ...

  2. cb01a_c++_数据结构_顺序容器_STL_deque类

    /*cb01a_c++_数据结构_顺序容器_STL_deque类deque是一个动态数组,比vector更加灵活.两者都属于动态数组deque与vector非常类似deque可以在数组开头和末尾插入和 ...

  3. c++ UDP套接字客服端代码示范

    c++ UDP套接字客服端代码示范 #include<winsock2.h> //包含头文件 #include<stdio.h> #include<windows.h&g ...

  4. jmeter正则提取器提取一个值或多个值

    [安装Dummy插件] 这个插件可以模拟服务器返回,相当于一个mockserver了. 首先安装Dummy,选项--插件管理--可选插件--Dummy. [模拟响应] 添加线程组,在线程组下添加Dum ...

  5. Kubernetes学习笔记(九):StatefulSet--部署有状态的多副本应用

    StatefulSet如何提供稳定的网络标识和状态 ReplicaSet中的Pod都是无状态,可随意替代的.又因为ReplicaSet中的Pod是根据模板生成的多副本,无法对每个副本都指定单独的PVC ...

  6. opencv c++访问某一区域

    int main(){ cv::Mat m1(,, CV_8UC1); for(int i=;i<m1.rows;++i) for(int j=;j<m1.cols;++j) m1.at& ...

  7. TCP实战二(半连接队列、全连接队列)

    TCP实验一我们利用了tcpdump以及Wireshark对TCP三次握手.四次挥手.流量控制做了深入的分析,今天就让我们一同深入理解TCP三次握手中两个重要的结构:半连接队列.全连接队列. 参考文献 ...

  8. NFC芯片选型及基本电路框架

    RFID作为一项专业度较高的技术,在一些公司,可能还会专门招聘专业的RFID工程师.本篇阐述的涉及到的只是基本选型设计.电路框架,关于RFID天线调试.低功耗检卡调试等,后续再其他篇章会继续更新! N ...

  9. Java工具类—包装类

    Java工具类--包装类 我们都知道,JDK 其实给我们提供了很多很多 Java 开发者已经写好的现成的类,他们其实都可以理解成工具类,比如我们常见的集合类,日期相关的类,数学相关的类等等,有了这些工 ...

  10. mybatis源码配置文件解析之五:解析mappers标签流程图

    前面几篇博客分析了mybatis解析mappers标签的过程,主要分为解析package和mapper子标签.补充一张解析的总体过程流程图,画的不好,多多谅解,感谢.