什么是分布式锁

在单机部署的情况下,要想保证特定业务在顺序执行,通过JDK提供的synchronized关键字、Semaphore、ReentrantLock,或者我们也可以基于AQS定制化锁。单机部署的情况下,锁是在多线程之间共享的,但是分布式部署的情况下,锁是多进程之间共享的。那么分布式锁要保证锁资源的唯一性,可以在多进程之间共享。

分布式锁特性

  • 保证同一个方法在某一时刻只能在一台机器里一个进程中一个线程执行;
  • 要保证是可重入锁(避免死锁);
  • 要保证获取锁和释放锁的高可用;

分布式锁实现

  • 锁释放(finally);
  • 锁超时设置;
  • 锁刷新(定时任务,每2/3的锁生命周期执行);
  • 如果锁超时了,防止删除其他线程的锁(其他线程会拿到锁),考虑 value值用线程id标识,当前线程释放锁的时候要判断是否为当前线程的线程id;
  • 可重入;

Redis分布式锁

RedisLockRegistry

RedisLockRegistry是spring-integration-redis中提供redis分布式锁实现类。主要是通过redis锁+本地锁双重锁的方式实现的一个比较好的锁。

OBTAIN_LOCK_SCRIPT是一个上锁的lua脚本。KEYS[1]代表当前锁的key值,ARGV[1]代表当前的客户端标识,ARGV[2]代表过期时间。

基本逻辑是:根据KEYS[1]从redis中拿到对应的客户端标识,如已存在的客户端标识和ARGV[1]相等,那么重置过期时间为ARGV[2];如果值不存在,设置KEYS[1]对应的值为ARGV[1],并且过期时间是ARGV[2]。

获取锁的过程也很简单,首先通过本地锁(localLock,对应的是ReentrantLock实例)获取锁,然后通过RedisTemplate执行OBTAIN_LOCK_SCRIPT脚本获取redis锁。

为什么要使用本地锁呢,首先是为了锁的可重入,其次是减轻redis服务压力。

释放锁的过程也比较简单,第一步通过本地锁判断当前线程是否持有锁,第二步通过本地锁判断当前线程持有锁的计数。

如果当前线程持有锁的计数 > 1,说明本地锁被当前线程多次获取,这时只释放本地锁(释放之后当前线程持有锁的计数-1)。

如果当前线程持有锁的计数 = 1,释放本地锁和redis锁。

RedisLockRegistry使用如上所示。

首先定义RedisLockRegistry对应的Bean,需要依赖redis的ConnectionFactory。

然后在服务层中注入RedisLockRegistry实例。

通过lock方法和unlock方法将业务逻辑包起来,需要注意的是unlock方法要写在finally代码块中。

Redisson

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。

充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。

使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。

同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

  

首先感受一下通过Redisson Api使用redis分布式锁。

定义RedissonBuilder,通过redis集群地址构建RedissonClient。

定义RedissonClient类型的Bean。

业务代码里,通过RedissonClient获取分布式锁。

由于对Redisson分布式锁实现原理了解的也不是很透彻,这里推荐一篇文章:Redisson 分布式锁实现分析

Redisson和RedisLockRegistry对比

  • RedisLockRegistry通过本地锁(ReentrantLock)和redis锁,双重锁实现,Redission通过Netty Future机制、Semaphore (jdk信号量)、redis锁实现。
  • RedisLockRegistry和Redssion都是实现的可重入锁。
  • RedisLockRegistry对锁的刷新没有处理,Redisson通过Netty的TimerTask、Timeout 工具完成锁的定期刷新任务。
  • RedisLockRegistry仅仅是实现了分布式锁,而Redisson处理分布式锁,还提供了了队列、集合、列表等丰富的API。

动手实现分布式锁

实现原理

本地锁(ReentrantLock)+ redis锁

获取锁lua脚本

锁刷新lua脚本

锁释放lua脚本

本地锁定义

每一个lock key对应唯一的一个本地锁

线程标识定义

分布式环境下,每一个线程对应一个唯一标识

锁刷新定时任务定义

通过JDK ConcurrentTaskScheduler完成定时任务执行,ScheduledFuture完成定时任务销毁。其中taskId对应线程标识。

定义分布式锁注解

分布式锁切面

通过RedisLock注解实例lockInfo获取到锁key值、锁过期时间信息。

获取锁过程

  1. 通过lockInfo.key()方法获取到锁key值,通过锁key值拿到对应的本地锁(ReentrantLock)
  2. 本地锁获取锁对象
  3. 进入获取redis锁的循环
  4. 通过缓存服务组件执行获取锁的lua脚本
  5. 如果获取到redis锁,判断当前线程是否第一次获取到锁并且开启了锁刷新,相应的注册锁刷新定时任务
  6. 如果没有获取到redis锁,休眠lockInfo.sleep()毫秒的时间,再次重试

释放锁过程

  1. 获取到当前锁key值对应的本地锁
  2. 判断当前线程是否为本地锁锁的持有者
  3. 如果本地锁的重入次数大于1,则只释放本地锁
  4. 如果本地锁的重入次数等于1,释放本地锁和redis锁

分布式锁测试

定义测试类,测试方法注上@RedisLock注解,制定锁的key值为 "redis-lock-test",测试方法内随机休眠。

开启20个线程,同时调用测试方法。

多线程redis分布式锁测试结果如下。

  

定义可重入测试类,方法内获取当前代理对象,递归调用测试方法。

测试方法中,调用可重入测试类注有@RedisLock的测试方法。

分布式锁可重入测试结果如下。

Redis分布式锁实战的更多相关文章

  1. 不用找了,基于 Redis 的分布式锁实战来了!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 作者:菜蚜 my.oschina.net/wnjustdoit/blog/1606215 前言:在分布式环境中,我们经常使用 ...

  2. 基于SpringBoot AOP面向切面编程实现Redis分布式锁

    基于SpringBoot AOP面向切面编程实现Redis分布式锁 基于SpringBoot AOP面向切面编程实现Redis分布式锁 基于SpringBoot AOP面向切面编程实现Redis分布式 ...

  3. 手撕redis分布式锁,隔壁张小帅都看懂了!

    前言 上一篇老猫和小伙伴们分享了为什么要使用分布式锁以及分布式锁的实现思路原理,目前我们主要采用第三方的组件作为分布式锁的工具.上一篇运用了Mysql中的select ...for update实现了 ...

  4. Redisson 分布式锁实战与 watch dog 机制解读

    Redisson 分布式锁实战与 watch dog 机制解读 目录 Redisson 分布式锁实战与 watch dog 机制解读 背景 普通的 Redis 分布式锁的缺陷 Redisson 提供的 ...

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

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

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

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

  7. 从Redis分布式缓存实战入手到底层原理分析、面面俱到覆盖大厂面试考点

    概述 官方说明 Redis官网 https://redis.io/ 最新版本6.2.6 Redis中文官网 http://www.redis.cn/ 不过中文官网的同步更新维护相对要滞后不少时间,但对 ...

  8. 利用redis分布式锁的功能来实现定时器的分布式

    文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...

  9. Redis分布式锁

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

随机推荐

  1. Python开发【第七篇】: 面向对象和模块补充

    内容概要 特殊成员 反射 configparser模块 hashlib模块 logging模块 异常处理 模块 包 1. 特殊成员 什么是特殊成员呢? __init_()就是个特殊的成员. 带双下划线 ...

  2. Python开发【第五篇】: 内置模块

    内容概要 二分查找.冒泡 random time os sys pickle json shelve re 1.二分查找和冒泡排序 01. 二分查找 二分查找也称折半查找(Binary Search) ...

  3. Java基础中字符串与字符的注意点!

    在Java中,字符的表达与字符串的表达是不一样的!话不多说,直接上代码!!! String  a="a"; char a='a'; 其中他们的引号是不一样的

  4. 201907 TIOBE 编程语言排行榜-Python坐稳第三

    目录 一.编程语言7月排行榜 二.Top10编程语言指数走势(2002-2018) 三.历史排名(1988-2019) 四.编程语言"名人榜"( 2003-2018) 五.Top2 ...

  5. 分享SQL,查询用户最近一次购买时间间隔

    (1)先创建一张测试表: CREATE TABLE `用户购买订单` ( `购买时间` datetime(6) NULL DEFAULT NULL, `用户` varchar(20) CHARACTE ...

  6. CentOS 操作防火墙

    1:查看防火状态 systemctl status firewalld 2:暂时关闭防火墙 systemctl stop firewalld 3:永久关闭防火墙 systemctl disable f ...

  7. JAVA写入大文件DEMO

    /**     * 读取filePath的文件     * @param filePath    文件的路径     * @return     List集合       文件中一行一行的数据     ...

  8. JS高级程序设计第2章--精简版

    前言:这次是二刷了,想暑假做一次完整的笔记,但用本子来写笔记的话太贵了,可能哪天还丢了..所以还是博客好== 第二章:在HTML中使用JavaScript 2.1 <script>元素: ...

  9. 简书全站爬取 mysql异步保存

    # 简书网 # 数据保存在mysql中; 将selenium+chromedriver集成到scrapy; 整个网站数据爬取 # 抓取ajax数据 #爬虫文件 # -*- coding: utf-8 ...

  10. 音频算法speex中的aec分析以及解析

    算法原理: Speex的AEC是以NLMS(Normalized Least Mean Square)为基础,用MDF(multidelay block frequency domain)频域实现,最 ...