做网站的服务器用什么 系统好,人人开发app,2022年热点营销案例,加快门户网站建设本博客为个人学习笔记#xff0c;学习网站与详细见#xff1a;黑马程序员Redis入门到实战 P56 - P63 目录
分布式锁介绍
基于SETNX的分布式锁
SETNX锁代码实现
修改业务代码
SETNX锁误删问题
SETNX锁原子性问题
Lua脚本
编写脚本
代码优化
总结
Redisson
前言… 本博客为个人学习笔记学习网站与详细见黑马程序员Redis入门到实战 P56 - P63 目录
分布式锁介绍
基于SETNX的分布式锁
SETNX锁代码实现
修改业务代码
SETNX锁误删问题
SETNX锁原子性问题
Lua脚本
编写脚本
代码优化
总结
Redisson
前言
介绍
Redisson快速入门
原理介绍
Redission可重入锁原理
Redission锁重试和WatchDog机制 分布式锁介绍 在上一篇文章 Redis实战—优惠卷秒杀 中我们通过使用锁、事务和代理对象实现了“一人一单”的优惠券秒杀功能。但我们使用的锁是基于JVM内部的锁这导致锁的范围只能限制单个JVM的线程操作因此在集群情况下依然会出现超卖问题。所以我们需要设置一个锁使其能够同时限制集群中的多个JVM线程操作而这个锁就是分布式锁由此引出本文。
集群情况下JVM锁的使用情况如下图。 集群情况下分布式锁的使用情况如下图。 分布式锁的实现 基于SETNX的分布式锁 我们利用Redis的SET lock thread1 NX操作来模拟获取锁即如果当前不存在lock键则添加lock键成功如果当前存在lock键则添加lock键失败。我们将添加lock键的操作视为获取锁的操作将lock键是否存在视为当前锁是否已被其他线程获取。执行语句后通过Redis返回OK或者nil我们可以判断是否获取锁成功。为防止宕机时无法对锁进行销毁我们在进行SET操作时还需通过EX为键设置一个合理的时间。 SETNX锁代码实现
// 接口类
public interface ILock {/** 尝试获取锁* timeoutSec 锁持有的超时时间过期后自动释放* 返回值 true代表获取锁成功false代表获取锁失败* */boolean tryLock(long timeoutSec);//释放锁void unlock();}// 接口实现类
public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}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);//避免拆箱导致空指针使用Boolean.TRUE.equals方法返回结果return Boolean.TRUE.equals(success);}Overridepublic void unlock() {// 释放锁stringRedisTemplate.delete(KEY_PREFIX name);}
}修改业务代码 public Result seckillVoucher(Long voucherId) {//判断是否满足抢购条件...Long userId UserHolder.getUser().getId();// 创建锁对象根据用户ID加锁SimpleRedisLock lock new SimpleRedisLock(order: userId, stringRedisTemplate);// 获取锁boolean isLock lock.tryLock(1200);// 若获取锁失败if (!isLock)return Result.fail(不允许重复下单);// 若获取锁成功try {// 获取当前代理对象事务IVoucherOrderService proxy (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {lock.unlock();}} SETNX锁误删问题 如上图所示持有锁的线程1在锁的内部出现了业务阻塞导致它的锁被超时释放。这时线程2尝试获得锁成功然而在线程2持有锁执行过程中线程1的业务反应过来继续执行而线程1业务执行完成后进行了删除锁逻辑此时就会把本应属于线程2的锁进行删除这就是误删其它线程锁的情况。 解决方案当线程创建锁时同时为该锁添加当前线程标识该标识由UUID随机数为前缀与线程id组合而成为避免出现集群下两个线程的id相同的情况因此添加UUID前缀。当一个线程删除锁时需要判断当前线程标识与锁标识是否一致若一致说明该锁由当前线程创建可进行删除若不一致说明该锁由其它线程创建不可进行删除。 对simpleRedisLock类代码优化如下。
package com.hmdp.utils;import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}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);//避免拆箱导致空指针使用Boolean.TRUE.equals方法返回结果return Boolean.TRUE.equals(success);}Overridepublic void unlock() {// 获取线程标识String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁标识String lockID stringRedisTemplate.opsForValue().get(KEY_PREFIX name);// 判断标识是否一致if(threadId.equals(lockID))stringRedisTemplate.delete(KEY_PREFIX name);}
}SETNX锁原子性问题 如上图所示线程1执行业务结束后进行释放锁的操作在对锁的标识进行判断后开始释放锁。但是线程1在判断结束到释放锁的期间受到了阻塞遇到JVM垃圾回收机制时会暂停程序导致阻塞这时线程2获取锁。当线程1恢复后继续进行释放锁的操作将会误删线程2的锁。我们前面设置了锁标识并且要求在释放锁之前需要做一个判断但在判断可以释放锁后如果遇到了阻塞将可能导致上图所示的误删操作。 解决方法我们需要实现判断和释放锁这两条命令的原子性操作。 Lua脚本 Redis提供了Lua脚本功能在一个脚本中编写多条Redis命令能够确保多条命令执行时的原子性。Lua是一种编程语言其基本语法可以参考网站Lua 教程 | 菜鸟教程。这里重点介绍Redis提供的调用函数我们可以使用lua去操作redis以保证多条redis命令的原子性这样就可以实现拿锁、判断、删锁多条命令的原子性动作了作为一名Java程序员这一块并不需要大家过于精通只需要知道它有什么作用即可。 编写脚本 我们需要在resources文件中新建.lua文件如果没有该新建项需要下载EmmyLua插件并在其中添加下图中的脚本内容。 代码优化 优化后的代码如下。
public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}private static final String KEY_PREFIX lock:;private static final String ID_PREFIX UUID.randomUUID().toString(true) -;private static final DefaultRedisScriptLong UNLOCK_SCRIPT;//初始化UNLOCK_SCRIPTstatic {UNLOCK_SCRIPT new DefaultRedisScript();UNLOCK_SCRIPT.setLocation(new ClassPathResource(unlock.lua));//初始化返回值UNLOCK_SCRIPT.setResultType(Long.class);}Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标识String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁并设置锁标识、添加时间Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name, threadId, timeoutSec, TimeUnit.SECONDS);//避免拆箱导致空指针使用Boolean.TRUE.equals方法返回结果return Boolean.TRUE.equals(success);}Overridepublic void unlock() {// 调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,//要求传入KEYS集合使用Collections单元素集合工具Collections.singletonList(KEY_PREFIX name),//线程标识ID_PREFIX Thread.currentThread().getId());}/* Overridepublic void unlock() {// 获取线程标识String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁标识String lockID stringRedisTemplate.opsForValue().get(KEY_PREFIX name);// 判断标识是否一致if(threadId.equals(lockID))stringRedisTemplate.delete(KEY_PREFIX name);}*/
} 总结
基于Redis的分布式锁实现思路 · 利用set nxex获取锁并设置过期时间保存线程标识 · 释放锁时先判断线程标识是否与锁标识一致若一致则删除锁
特性 · 利用set nx满足互斥性 · 利用set ex保证故障时锁依然能释放避免死锁提高安全性 · 利用redis集群保证高可用和高并发特性本文未涉及 Redisson
前言
基于SETNX实现的分布式锁存在以下的问题
重入问题重入问题是指获得锁的线程可以再次进入到相同的锁的代码块中可重入锁的意义在于防止死锁比如HashTable的代码中方法都是使用synchronized修饰的。假如它在一个方法内调用另一个方法如果此时是不可重入的就会导致死锁。所以可重入锁的主要意义是防止死锁我们的synchronized和Lock锁都是可重入的。
不可重试是指目前的分布式只能尝试一次获取锁我们认为合理的情况是当线程在获得锁失败后它应该能再次尝试获取锁。
超时释放我们在加锁时添加了过期时间目的是防止死锁但如果阻塞的时间超长尽管我们采用了lua表达式防止误删其它锁但因为阻塞锁被超时释放没有锁住依然有安全隐患。
主从一致性 如果Redis提供了主从集群当我们向集群写数据时主机需要异步的将数据同步给从机而万一在同步过去之前主机宕机了将会导致死锁问题。 介绍 Redisson是一个在Redis基础上实现的Java驻内存数据网格In-Memory Data Grid。它不仅提供了一系列的分布式的Java常用对象还提供了许多分布式服务其中就包含了各种分布式锁的实现。 Redisson快速入门
一、引入依赖
dependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.13.6/version
/dependency
二、配置Redisson客户端
Configuration
public class RedissonConfig {Beanpublic RedissonClient redissonClient(){// 配置Config config new Config();config.useSingleServer().setAddress(redis://192.168.150.101:6379).setPassword(123321);// 创建RedissonClient对象return Redisson.create(config);}
}
三、基本使用方法
Resource
private RedissionClient redissonClient;Test
void testRedisson() throws Exception{//获取锁(可重入)指定锁的名称RLock lock redissonClient.getLock(anyLock);//尝试获取锁参数分别是获取锁的最大等待时间(期间会重试)锁自动释放时间时间单位//若无参数默认为-130s 即不重试获取锁自动释放时间为30sboolean isLock lock.tryLock(1,10,TimeUnit.SECONDS);//判断获取锁成功if(isLock){try{System.out.println(执行业务); }finally{//释放锁lock.unlock();}}
} 原理介绍 原理篇章详细见黑马程序员Redis入门到实战 P66 - P68 深入学习建议看视频 Redission可重入锁原理 在Lock锁中它是借助于底层的一个voaltile的一个state变量来记录重入状态的比如当前没有线程持有这把锁那么state0假如有线程持有这把锁那么state1。如果持有这把锁的线程再次持有这把锁那么state就会1 。如果是对于synchronized而言它在c语言代码中会有一个count其原理和state类似也是重入一次就1释放一次就-1 直到值为0 时表示当前这把锁没有被任何线程持有。 而Redission也支持可重入锁其底层采用lua脚本实现原理与上述内容相似它采用hash结构来存储锁其中大key表示当前这把锁被哪个线程持有小key表示这把锁是否存在如果持有这把锁的线程再次持有这把锁那么小key就会1。 Redission锁重试和WatchDog机制
很复杂建议多看几遍视频 Redisson分布式锁原理 可重入利用hash结构记录线程id和重入次数 可重试利用信号量和PubSub功能实现等待、唤醒获取锁失败的重试机制 超时续约利用watchDog每隔一段时间(releaseTime/3)重置超时时间