黑马点评–分布式锁

基本原理与不同实现方式对比:

什么是分布式锁:

​ 分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:

基于Redis的分布式锁

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:

    • 互斥:确保只能有一个线程获取锁

    • set lock thread1 nx ex 10
      
  • 释放锁:

    • 手动释放

    • 超时释放:获取锁时添加一个超时时间

    • Del key
      

流程:

基于Redis实现分布式锁初级版本:

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能

public interface ILock {/*** 尝试获取锁* @param timeoutSec 锁持有的超时时间,过期后自动释放* @return true代表获取锁成功;false代表获取锁失败*/boolean tryLock(long timeoutSec);/*** 释放锁*/void unlock();
}

实现ILock接口:

public class SimpleRedisLock implements ILock{private StringRedisTemplate stringRedisTemplate;//锁的名称private String name;public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}//锁的前缀private static final String KEY_PREFIX ="lock:";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程表示long threadId = Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//释放锁stringRedisTemplate.delete(KEY_PREFIX+name);}
}

对秒杀劵一人一单进行分布式锁实现:

  @Autowiredprivate ISeckillVoucherService iSeckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result seckillVoucher(Long voucherId) {//1.查询优惠劵SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);//2.判断秒杀是否开始LocalDateTime beginTime = voucher.getBeginTime();if (beginTime.isAfter(LocalDateTime.now())) {//尚未开始return Result.fail("活动尚未开始");}//3.判断秒杀是否已经结束LocalDateTime endTime = voucher.getEndTime();if (LocalDateTime.now().isAfter(endTime)) {//已结束return Result.fail("活动已经结束");}//4判断库存是否充足if (voucher.getStock() < 1) {//库存不足return Result.fail("库存不足!");}Long userId = UserHolder.getUser().getId();
//        synchronized (userId.toString().intern()){//创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁boolean tryLock = lock.tryLock(1200);//判断获取锁成功if (!tryLock){//获取锁失败,返回错误或重试return Result.fail("一个人允许下一单");}try {//获取spring事务代理对象IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} catch (IllegalStateException e) {e.printStackTrace();}finally {//释放锁lock.unlock();}
//    }return Result.fail("抢购失败");}@Transactionalpublic  Result createVoucherOrder(Long voucherId) {//6.一个人一单Long userId = UserHolder.getUser().getId();//6.1查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//6.2判断是否存在if (count > 0) {//用户以及购买过return Result.fail("用户已经购买过一次");}//7.扣减库存boolean success = iSeckillVoucherService.update().setSql("stock =stock -1").eq("voucher_id", voucherId).gt("stock", 0).update();if (!success) {//扣减失败return Result.fail("库存不足!");}//8.创建订单VoucherOrder voucherOrder = new VoucherOrder();//8.1 订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//8.2 用户idvoucherOrder.setUserId(userId);//8.3 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 9.返回订单idreturn Result.ok(orderId);}

解决Redis分布式锁误删问题:

需求:修改之前的分布式锁实现,满足:

  1. 在获取锁时存入线程标示(可以用UUID表示)
  2. 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
    • 如果一致则释放锁
    • 如果不一致则不释放锁
public class SimpleRedisLock implements ILock{private StringRedisTemplate stringRedisTemplate;//锁的名称private String name;public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;this.name = name;}//锁的前缀private static final String KEY_PREFIX ="lock:";private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程表示String threadId =ID_PREFIX+Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+name,threadId+"", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unlock() {//获取线程标示String threadId =ID_PREFIX+Thread.currentThread().getId();// 获取锁中的标示String id =stringRedisTemplate.opsForValue().get(KEY_PREFIX+name);//判断标示是否一致if (threadId.equals(id)){//释放锁stringRedisTemplate.delete(KEY_PREFIX+name);}}
}

分布式锁的原子性问题:

当获取锁标示并判断是一致时,jvm执行gc时改线程发生阻塞,导致没有及时释放锁。如果在阻塞阶段锁超时释放,就会导致其他线程获得到锁。这时如果改线程阻塞结束,去释放锁就会导致误释放其他线程的锁。引发线程安全问题。

解决方法:使判断锁和释放锁为原子性(同成功,同时失败)

Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多余Redis命令,确保多条命令执行时的原子性。

Redis提供的调用函数,语法如下:

redis.call('命令','key','其它参数',...)

例如,执行set name jack 脚本为:

redis.cll('set','name','jack')

再次改进Redis的分布式锁:

需求:基于Lua脚本实现分布式锁的释放锁逻辑

提示:RedisTemplate调用Lua脚本的API如下:

释放锁的逻辑改变

  private static final DefaultRedisScript<Long> UNLOCK_SCRIPT ;static {UNLOCK_SCRIPT =new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}
@Overridepublic void unlock() {//调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX+name),ID_PREFIX+Thread.currentThread().getId());}

lua脚本:

-- 比较线程标示与锁中的标示是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then--释放锁 del keyreturn redis.call('del',KEYS[1])
end
return 0

总结基于Redis的分布式锁实现思路:

  • 利用set nx ex获取锁,并设置过期时间,保存线程标示
  • 释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

  • 利用set nx满足互斥性
  • 利用set ex保证故障时锁依然能释放。避免死锁,提高安全性
  • 利用Redis集群保证高可用和高并发特性

基于Redis的分布式锁优化:

基于setnx实现的分布式锁存在下面的问题:

Redisson:

Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redisson快速入门:

1.引入依赖:

        <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version></dependency>

2.配置Redisson客户端:

@Configuration
public class RedisConfig {@Beanpublic RedissonClient redissonClient(){//配置类Config config=new Config();//添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址config.useSingleServer().setAddress("redis://43.138.50.132:6379").setPassword("123321");//创建客户端return Redisson.create(config);}
}

3.使用Redisson的分布式锁

 @Resourceprivate RedissonClient redissonClient;@Testvoid testRedisson() throws InterruptedException{//获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("anyLock");//尝试获取锁,参数分别:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位boolean isLock =lock.tryLock(1,10, TimeUnit.SECONDS);//判断释放获取成功if (isLock){try {System.out.println("执行业务");}finally {//释放锁lock.unlock();}}}

Redisson可重入锁原理:

锁的存储使用hash结构

获取锁:

释放锁:

基于setnx实现的分布式锁存在下面的问题:

Redisson分布式锁原理:

  • 可重入:利用hash结构记录线程id和重入次数
  • 可重试:利用信号量和PubSub功能实现等待,唤醒,获取锁失败的重试机制
  • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

Redisson的multiLock解决:

分布式锁主从一致性问题----没听懂。。。

总结:

不可重入Redis分布式锁:

  • 原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示
  • 缺陷:不可重入,无法重试,锁超时失效

可重入的Redis分布式锁:

  • 原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制重试等待
  • 缺陷:redis宕机引起锁失效问题

Redisson的multiLock:

  • 原理:多个独立的Redis节点,必须在所有节点都获取重入锁,才算获取锁成功
  • 缺陷:运维成本高,实现复杂

黑马点评--分布式锁相关推荐

  1. 【Redis | 黑马点评 + 思维导图】分布式锁

    文章目录 分布式锁的基本原理和实现方式对比 Redis分布式锁的实现核心思路 分布式锁的初级实现 Redis分布式锁误删情况说明 解决Redis分布式锁误删问题 分布式锁的原子性问题 Lua脚本解决多 ...

  2. Redis分布式锁 | 黑马点评

    目录 一.分布式锁概述 二.基于Redis的分布式锁 1.思路分析 2.初级版本 3.误删问题 4.改进分布式锁 5.原子性问题 6.使用Lua脚本解决原子性问题 7.setnx实现分布式锁存在问题 ...

  3. 黑马点评Redis实战(短信登录;商户查询缓存)

    黑马点评 通过一个类似于大众点评的项目了解学习redis在实战项目中的使用,下面是项目中会涉及到的模块: 一.导入黑马点评项目 导入springboot项目,导入sql脚本到数据库,开启nginx,更 ...

  4. 黑马点评:商户查询缓存

    文章目录 前言 缓存介绍 什么是缓存? 为什么要使用缓存? 如何使用缓存? 添加商户缓存 原始方法 缓存模型和思路 代码实现 缓存更新策略 数据库缓存不一致解决方案 数据库和缓存不一致采用什么方案 实 ...

  5. 黑马点评的总结(还在更新...)

    黑马点评的总结和反思 1.缓存穿透 问题的体现 *下面就是我们的解决的方法(一旦查询到一次不存在的值,就往redis里面放入我们的空字符串这样下次访问无效的数据就可以使用redis来返回空字符串来防止 ...

  6. 黑马点评-优惠券秒杀

    目录 全局唯一ID生成 代码实现 ①编写工具类 ②编写测试类 添加优惠券-利用postman发送请求达成添加功能 VoucherOrderController VoucherOrderServiceI ...

  7. Redis学习笔记②实战篇_黑马点评项目

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 资料链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA( ...

  8. 黑马点评项目全部功能实现及详细笔记--Redis练手项目

    目录 一.项目详情 1.1 项目简介 1.2 数据库表设计 1.3 前端部署 1.4 后端搭建 二.短信登录 2.1 发送验证码 2.2 验证码登录 2.3 登录校验拦截器 2.4 退出登录(补充) ...

  9. 个人项目总结-瑞吉外卖/传智健康/黑马点评

    1. 瑞吉外卖 瑞吉外卖技术栈:SpringBoot.MybatisPlus.springMVC 瑞吉外卖是我做的第一个项目,算是我做过所有的项目中最简单的,很适合新手入门,我当时是学完springb ...

最新文章

  1. 软件开发 理想_我如何在12个月内找到理想的软件工作
  2. 60行代码爬取知乎“神回复”,句句戳中泪点
  3. 窃喜,第一次修改开源的东西
  4. gradle更换仓库 解决下载速度慢问题
  5. xstream解析xml字符串和生成对象
  6. 【项目管理】用LoC衡量程序员的工作效率是不科学的
  7. HandleExternalEventActivity
  8. 做外贸出口,要想快速开发客户,快速赚大钱
  9. 剑指Offer之从上往下打印二叉树
  10. mysql主从复制及读写分离
  11. svn的安装包和中文语言包下载
  12. 浅析麒麟信安云几大优势之“安全性”篇
  13. 2019主流浏览器市场占有率及其内核
  14. 拜读经典——大话设计模式(一)——温习C#
  15. 对称复曲线(直线、缓曲、圆曲、缓曲、直线)中边桩坐标计算
  16. 共享办公室,月赚2万-5万的阳光创业项目
  17. Talent Plan TinyKV Project1 StandaloneKV
  18. 用过最好用的swf格式视频转换器,swf转成mp4
  19. python 打开文件夹_python打开目录
  20. matlab仿真时三相电流设置,三相输入电流波形与三相整流MATLAB仿真

热门文章

  1. 基于云计算的视频会议系统特点与价值
  2. tolua lua 添加 C库的byte[] 解析 byteArray ipack
  3. ats 与 https
  4. 2022年10月各大学网络教育统考计算机应用基础考试题库及辅导
  5. cad调了比例因子没反应_如何在CAD中引用外部图片
  6. 飞桨常规赛:PALM眼底彩照中黄斑中央凹定位-11月第1名方案
  7. ccf 202303-1 田地丈量 c语言简单方法
  8. 基于TLS协议的安全分析
  9. linux No targets specified and no makefile found
  10. 官方首发_ask2问答系统下载3.0下载