ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

Redis学习日记(五)

Redis学习日记(五) Redis实战篇一、分布式锁1.定义回答满足分布式系统或集群模式下多进程可见并且互斥的锁。2.核心思想回答分布式锁的核心思想就是让大家都使用同一把锁只要大家使用的是同一把锁那么我们就能锁住线程不让线程进行让程序串行执行这就是分布式锁的核心思路。3.基本条件回答分布式锁满足的条件主要有以下几个方面1可见性多个线程都能看到相同的结果注意这个地方说的可见性并不是并发编程中指的内存可见性只是说多个进程之间都能感知到变化的意思。2互斥互斥是分布式锁的最基本的条件使得程序串行执行。3高可用程序不易崩溃时时刻刻都保证较高的可用性。4高性能由于加锁本身就让性能降低所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能。5安全性安全也是程序中必不可少的一环。4.分布式锁的实现回答分布式锁的核心思想是多进程之间互斥而满足这一点的方式有很多常见的三种选取基于Redis实现分布式锁需要实现两个基本方法获取锁和释放锁获取锁互斥确保只能有一个线程获取锁通过setnx来完成非阻塞尝试一次成功返回true失败返回false释放锁手动释放通过del来完成超时释放获取锁时添加一个超时时间具体实现方式我们利用redis 的setNx 方法当有多个线程进入时我们就利用该方法第一个线程进入时redis 中就有这个key 了返回了1如果结果是1则表示他抢到了锁那么他去执行业务然后再删除锁退出锁逻辑没有抢到锁的哥们等待一定时间后重试即可流程图如下图所示5.核心代码//核心代码 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:; Override public boolean tryLock(long timeoutSec) { //获取线程id long threadId Thread.currentThread().getId(); //获取锁 Boolean success stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX name, threadId , timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } Override public void unlock() { //释放锁 stringRedisTemplate.delete(KEY_PREFIX name); }5.Redis分布式锁误删情况说明回答逻辑说明持有锁的线程在锁的内部出现了阻塞导致他的锁自动释放这时其他线程线程2来尝试获得锁就拿到了这把锁然后线程2在持有锁执行过程中线程1反应过来继续执行而线程1执行过程中走到了删除锁逻辑此时就会把本应该属于线程2的锁进行删除这就是误删别人锁的情况说明。解决方案解决方案就是在每个线程释放锁的时候去判断一下当前这把锁是否属于自己如果属于自己则不进行锁的删除假设还是上边的情况线程1卡顿锁自动释放线程2进入到锁的内部执行逻辑此时线程1反应过来然后删除锁但是线程1一看当前这把锁不是属于自己于是不进行删除锁逻辑当线程2走到删除锁逻辑时如果没有卡过自动释放锁的时间点则判断当前这把锁是属于自己的于是删除这把锁如下所示6.解决Redis分布式锁误删问题需求修改之前的分布式锁实现满足在获取锁时存入线程标示可以用UUID表示 在释放锁时先获取锁中的线程标示判断是否与当前线程标示一致如果一致则释放锁如果不一致则不释放锁核心逻辑在存入锁时放入自己线程的标识在删除锁时判断当前这把锁的标识是不是自己存入的如果是则进行删除如果不是则不进行删除。业务流程图变化如下所示核心代码//核心代码 //获取锁 private static final String ID_PREFIX UUID.randomUUID().toString(true) -; Override public 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); } //释放锁 public 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); } }7.分布式锁的原子性问题1说明回答更为极端的误删逻辑说明线程1现在持有锁之后在执行业务逻辑过程中他正准备删除锁而且已经走到了条件判断的过程中比如他已经拿到了当前这把锁确实是属于他自己的正准备删除锁但是此时他的锁到期了那么此时线程2进来但是线程1他会接着往后执行当他卡顿结束后他直接就会执行删除锁那行代码相当于条件判断并没有起到作用这就是删锁时的原子性问题之所以有这个问题是因为线程1的拿锁比锁删锁实际上并不是原子性的我们要防止刚才的情况如下图所示发生。2解决方案回答采用Lua脚本解决多条命令原子性问题。Redis提供了Lua脚本功能在一个脚本中编写多条Redis命令确保多条命令执行时的原子性。Lua是一种编程语言它的基本语法大家可以参考网站Lua 教程 | 菜鸟教程这里重点介绍Redis提供的调用函数我们可以使用lua去操作redis又能保证他的原子性这样就可以实现拿锁比锁删锁是一个原子性动作了作为Java程序员这一块并不作一个简单要求并不需要大家过于精通只需要知道他有什么作用即可。3利用Java代码调用Lua脚本改造分布式锁//核心代码 private static final DefaultRedisScriptLong UNLOCK_SCRIPT; static { UNLOCK_SCRIPT new DefaultRedisScript(); UNLOCK_SCRIPT.setLocation(new ClassPathResource(unlock.lua)); UNLOCK_SCRIPT.setResultType(Long.class); } public void unlock() { // 调用lua脚本 stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX name), ID_PREFIX Thread.currentThread().getId()); } //经过以上代码改造后我们就能够实现 拿锁比锁删锁的原子性动作了~二、分布式锁-redission1.简介回答Redisson是一个在Redis的基础上实现的Java驻内存数据网格In-Memory Data Grid。它不仅提供了一系列的分布式的Java常用对象还提供了许多分布式服务其中就包含了各种分布式锁的实现。Redission提供了分布式锁的多种多样的功能2.Redission快速入门第一步引入依赖dependency groupIdorg.redisson/groupId artifactIdredisson/artifactId version3.13.6/version /dependency第二步配置Redisson客户端Configuration public class RedissonConfig { Bean public RedissonClient redissonClient(){ // 配置 Config config new Config(); config.useSingleServer().setAddress(redis://192.168.150.101:6379) .setPassword(123321); // 创建RedissonClient对象 return Redisson.create(config); } }第三步使用Redission的分布式锁Resource private RedissionClient redissonClient; Test void testRedisson() throws Exception{ //获取锁(可重入)指定锁的名称 RLock lock redissonClient.getLock(anyLock); //尝试获取锁参数分别是获取锁的最大等待时间(期间会重试)锁自动释放时间时间单位 boolean isLock lock.tryLock(1,10,TimeUnit.SECONDS); //判断获取锁成功 if(isLock){ try{ System.out.println(执行业务); }finally{ //释放锁 lock.unlock(); } } }3.redission可重入锁原理回答在Lock锁中他是借助于底层的一个voaltile的一个state变量来记录重入的状态的比如当前没有人持有这把锁那么state0假如有人持有这把锁那么state1如果持有这把锁的人再次持有这把锁那么state就会1 如果是对于synchronized而言他在c语言代码中会有一个count原理和state类似也是重入一次就加一释放一次就-1 直到减少成0 时表示当前这把锁没有被人持有。在redission中我们的也支持支持可重入锁在分布式锁中他采用hash结构用来存储锁其中大key表示表示这把锁是否存在用小key表示当前这把锁被哪个线程持有所以接下来我们一起分析一下当前的这个lua表达式这个地方一共有以下 3 个参数KEYS[1] 锁名称ARGV[1] 锁失效时间ARGV[2] id : threadId; 锁的小keyexists: 判断数据是否存在 name是lock是否存在,如果0就表示当前这把锁不存在redis.call(hset, KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 写成一个hash结构Lock{id : threadId : 1}如果当前这把锁存在则第一个条件不满足再判断redis.call(hexists, KEYS[1], ARGV[2]) 1此时需要通过大key小key判断当前这把锁是否是属于自己的如果是自己的则进行redis.call(hincrby, KEYS[1], ARGV[2], 1)将当前这个锁的value进行1 redis.call(pexpire, KEYS[1], ARGV[1]); 然后再对其设置过期时间如果以上两个条件都不满足则表示当前这把锁抢锁失败最后返回pttl即为当前这把锁的失效时间。如果小伙帮们看了前边的源码 你会发现他会去判断当前这个方法的返回值是否为null如果是null则对应则前两个if对应的条件退出抢锁逻辑如果返回的不是null即走了第三个分支在源码处会进行while(true)的自旋抢锁。if (redis.call(exists, KEYS[1]) 0) then redis.call(hset, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; return redis.call(pttl, KEYS[1]);
返回列表