Redis学习笔记2

Redis 分布式锁

单Redis实例实现分布式锁

获取锁的命令:
SET key unique_value NX PX 30000
该命令仅在key不存在的时候才能被执行成功(NX选项),并且这个key有一个30s的失效时间(PX选项)。该key所对应的unique_value必须是全局唯一的,所有持有该key的用户所拥有的值都不能一样。这样做的目的是为了能更安全的释放锁,释放锁的时候使用脚本告诉redis:只有key存在并且存储的值和直飞难过的值一样才能删除成功。
解锁脚本的命令:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

使用该方式释放锁可以避免删除别的客户端获取成功的锁。比如有以下情况:客户端A获取资源锁,但是紧着着被一个别的操作阻塞了或者网络发生故障,当客户端A运行完毕后要释放锁时,原来的锁早已经超市并且被redis自动释放了,假设在此期间客户端B获取到了资源锁,如果仅使用DEL命令将key删除,那么这种情况下就会把客户端B的锁给删除掉。但是使用LUA脚本就不会出现该情况,因为脚本仅会删除key相等并且value值相等的key。
unique_value设置可以是一个随机数,也可以是request请求ID或者自定义规则的唯一值。
该方式有个问题就是如果操作的时间大于失效的时间或者因为其他原因导致锁失效但是操作未完成,那么该情况下别的客户端就有可能同时获取到该锁,也就无法保证操作的原子性,以及并发情况下数据的正确性。解决办法就是使用lua脚本对该锁进行时间续约,即在操作未完成之前刷新失效时间。

开原框架Redisson实现分布式锁

首先要引入依赖

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

使用的方式也很简单:

RLock lock=redisson.getLock("lockKey");
lock.lock();
lock.unlock();

在这里插入图片描述
图片源自网络,如有侵权,联系作者删除

上图很好的描述了redisson的加锁解锁的流程。
暂时不对源码进行分析,用语言描述一下加解锁过程。
加锁过程
客户端1请求获取锁"myLock",在集群中,redisson会通过hash算法选择一台master机器进行存储。然后redisson会发送一段lua脚本,脚本如下

"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]);"

使用lua脚本的原因:lua脚本可以保证所有的执行逻辑在redis中要么全部执行成功要么全部执行失败,保证了执行逻辑的原子性。对脚本重关键变量的解释:

  • KEYS[1]:代表加锁中指定的key,如上面的"myLock"。
  • ARGV[2]:代表的是加锁的客户端ID,由Redisson连接池中连接Id及当前线程Id组合而成,使用:分割。例如761a0c81-5e36-494c-a94d-24ae725b3747:2
  • ARGV[1]:代表的是锁key的默认生存时间,默认30s。
    脚本解释:
    第一个if,首先判断redis中是否存在key,如果不存在执行if里面的代码,首先是利用hash数据结构设置一个key,然后内容是ARGV[2]:1,伪结构如下
    "key":{ARGV[2]:1},紧接着对该key设置一个过期时间ARGV[1],最后返回nil。
    当第一个if不满足时执行第二个if,该语句会先对锁key以及该key拥有的唯一value进行判断,如果存在就会执行if里面的代码,首先是对该锁key下的参数ARGV[2]执行+1(锁的可重入操作)操作,然后刷新过期时间,最后返回nil。
    如果两个if都不满足,会返回锁key剩余的过期时间。
    锁的自动续约:
    在枷锁之后redisson会创建一个续约定时器(看门狗),该定时器是一个后台线程,每当监测到锁的剩余有效期不足锁超时时间的三分之一时,会刷新当前锁的超时时间,以达到续约目的。
    锁的竞争机制:
    在客户端1获取锁的时候,客户端2也来获取锁,执行了相同的脚本,通过分析脚本我们可以知道两个if条件都不会满足,会直接执行最后一句话,返回给客户端2当前锁的剩余生存时间,此时客户端2会订阅当前锁的释放消息并进入一个while循环,不断地尝试获取锁。
    解锁机制:
    redisson在解锁的时候也会发送一段脚本,内容如下:
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                    "end; " +
                    "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                    "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                    "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return nil;"

脚本中关键变量的解释:

  • KEYS[1]:代表当前锁的key。
  • ARGV[3]:代代表的是加锁的客户端ID,由Redisson连接池中连接Id及当前线程Id组合而成,使用:分割。例如761a0c81-5e36-494c-a94d-24ae725b3747:2。
  • ARGV[2]:代表锁的默认过期时间。
  • ARGV[1]:解锁消息,LockPubSub.UNLOCK_MESSAGE。

脚本解释:
第一个if语句,会判断当前的key和唯一客户端ID的组合是否存在,不存在就直接返回nil。
如果key+唯一客户端ID的组合存在,设置并获取该组合下的值,即重入锁的计数。
如果counter的变量大于0,则说明可重入锁还有地方没有进行解锁,redisson则刷新过期时间并返回0。
否则执行删除key耳朵操作并发布解锁消息,返回1,此步骤说明该可重入锁已经解锁成功,发布解锁消息是用来给那些订阅了该锁的竞争者一个通知,告诉他们该锁已经释放,可以进行重新获取了。
其他情况返回nil。

redisson分布式锁的缺点:
如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。
但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

官方RedLock实现分布式锁

Redisson已经对RedLock算法进行了封装,实现代码如下:

RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向3个redis实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
    // isLock = redLock.tryLock();
    // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    redLock.unlock();
}

核心加锁解锁逻辑和上面redisson实现分布式锁的逻辑一样。
中文网介绍:http://redis.cn/topics/distlock.html

Redis过期策略和淘汰机制

redis过期策略

定期删除策略

redis默认每隔100ms就会随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就进行删除。
相关的两个配置参数:
hz 10 //表示1s执行10次定期删除,即每100ms执行一次。
maxmemory-samples 5//代表随机抽取的样本数量,默认为5个
该策略带来的问题是:因为定期删除只是随机抽取部分key来检测,这样的话就会出现大量已经过期的key并没有被删除,这就是为什么有时候大量的key明明已经过了失效时间,但是redis的内存还是被大量占用的原因 ,为了解决这个问题,Redis又引入了“惰性删除策略”。

惰性删除策略

在获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且过期了就会进行删除,否则就会返回给你值。

“定期删除+惰性删除”可以保证过期的key一定被删掉,但是只能保证最终会被删掉,如果在此期间定期删除遗漏了大量过期的key,而且我们也没有去查询这些key,那么在很长一点时间内,这些过期的键值对就会占据redis大量内存空间,最终导致redis内存空间不足,为了解决这个问题,redis又引入了内存淘汰机制

内存淘汰机制

在redis.conf配置文件中有一行相关的配置来指定内存淘汰策略使用的算法
maxmemory-policy volatile-lru

淘汰策略

内存淘汰机制里面一共包含8中策略:

  1. no-eviction:当内存不足以容纳新写入的数据时,新写入的操作会报错,一般不采用,是redis默认配置。
  2. allkeys-lru:当内存不足以容纳新写入的数据时,在所有的key中移除最近最少使用的key,该方式常用。
  3. allkeys-random:当内存不足以容纳新写入的数据时,在所有的key中随机移除key。
  4. allkeys-lfu:当内存不足以容乃新写入的数据时,在所有的key中删除最不经常使用的key。
  5. volatile-lru:当内存不足以容纳新写入的数据时,在设置了过期时间的key中,移除最近最少使用的key。
  6. volatile-random:当内存不足以容纳新写入的数据时,在设置了过期时间的key中随机移除key。
  7. volatile-lfu:当内存不足以容纳新写入的数据时,在设置了过期时间的key中移除最不常用的ley。
  8. volatile-ttl:当内存不足以容纳新写入的数据时,在设置了过期时间的key中,优先移除过期时间最早(剩余存活时间最少)的key。

内存淘汰的过程

1.客户端执行一个新的指令,添加数据。
2.Redis检查内存使用量,如果大于maxmemory的限制,就通过配置的淘汰策略清理内存。
3.执行新的命令,重复上述过程。

其他场景中对过期key的处理

1.快照生成RDB文件时,过期的key不会被保存在RDB文件中。
2.服务器重启载入RDB文件时,master服务器载入RDB时,文件中未过期的键会被正常载入,过期的键则会被忽略。slave载入RDB文件时,文件中的所有键都会被载入,当主从同步时,再和master保持一致。
3.AOF文件写入时,当过期的key被删除,del命令也会被同步到AOF文件中。
4.AOF文件重写时,过期的key不会被记录到AOF文件中。
5.主从同步时,master服务器在删除了过期的key时,会向所有的slave发送一个del命令,slave收到通知后会执行del命令,删除指定的key,slave不会主动执行过期删除操作,只会同步master服务器的命令,这样做是同意、中心化的键删除策略,保证了主从服务器的数据一致性。

redis中使用的LRU和LFU算法

LRU算法:(least recently used)最近最少使用算法,它吧数据存放在链表中并按照最近访问的顺序排序,当某个可以被访问是就将此key移动到链表的头部,保证了最近访问的元素都在链表的头部或者前面,当链表满了之后,就将最近最少使用的数据丢弃,腾出空间用来存放新进入的元素。
因为LRU算法会消耗大量的内存,而redis又是一个以内存为基础的数据库,所以redis为了节省内存,采用了一种近似LRU的做法:给每个可以设置一个大小为24bit的属性字段,用来存放最近一次被访问的时间戳。然后随机采样(采样数量通过maxmemory_samples进行配置),从样本中淘汰掉最旧的数据,直到redis占用内存小于maxmemory为止。采样数量越大越接近于标准LRU算法,但同时也会带来性能的消耗。
在redis3.0以后增加了淘汰池,进一步提高了与LRU算法的相似度。淘汰池维护一个数组,数组大小与maxmemory_samples配置的数量一致,在每一次淘汰时,新随机抽取的样本会和淘汰池中的样本进行合并,从合并后的样本中选出最旧的一个进行淘汰,并从剩余的样本中选出与maxmemory_samples数量一致的相对较旧的样本存入淘汰池中,以备下一次淘汰使用。增加一个淘汰池相当于变相增加了样本的数量,从而提高了与LRU算法的相似度。
题外话:LinkedhashMap可以实现LRU算法,其构造参数中有一个accessOrder参数,其含义为:true-按照访问顺序排序,false-按照插入顺序排序.

LFU算法:(least frequently used)最不经常使用算法,该算法的思想是按照数据的访问频率进行排序,访问频率高的排在前面,访问频率的排在后面,淘汰的时候就会淘汰那些访问频率低的相同频率低的会淘汰最旧的。
在redis的LFU算法中,redis为每个key维护了一个访问计数器,每次key被访问的时候,计数器增大,当不被访问的时候,计数器就会随着时间进行衰减。
在redis.conf文件中,用户可以配置LFU算法相关的两个参数:
lfu-log-factor:计数器增长速度的负载因子,参数值设置的越大,key的计数器增长就越缓慢,因此就会要求高频访问才能保证不被淘汰。
lfu-decay-time:计数器缩减速度的负载因子,是一个一分钟为单位的数值,表示隔多久计数器减1。

Redis事务

Redis事务原理

在redis事务中,主要有以下几个命令构成:
WATCH,UNWATCH,MULTI,EXEC,DISCARD。
Redis是通过WATCH(此处采用的是乐观锁,主要为了提高性能,减少客户端等待)命令,来监测当前事务的数据是否被改变过,如果被修改了,整个事务会终止,不再执行。WATCH监测到有key被修改后,会将对应的所有客户端的标志位置为CLIENT_DIRTY_CAS,表示数据被修改,后续执行EXEC的时候则会被中断,从而实现事务。UNWATCH命令是取消对指定key的监视。MULTI命令会将客户端的标志位置为CLIENT_MULTI,表示事务开启,在该状态下,后续命令(除了MULTI/WATCH/DISCARD/EXEC)之外,其他的命令都会被保存到一个列表里面,直到EXEC或者DISCARD命令执行,如果中途出现了语法错误之类的命令,redis会将客户端标志为置为CLIENT_DIRTY_EXEC,后续执行EXEC的时候,如果标志位存在CLIENT_DIRTY_CAS或者CLIENT_DIRTY_EXEC,整个事务会被终止,不会执行任何命令。
注:redis事务并不支持事务回滚的特性,这里的不支持事务回滚指的是在执行EXEC时发现的命令错误,而在添加命令队列时就发生的错误,redis会直接报错,转换成java语言通俗一点讲就是,编译时错误(添加命令队列出错)和运行时错误(执行EXEC出错)。官方解释是,因为事务回滚属于复杂的功能,这和redis追求的简单高效的设置主旨不符合,并且解释中说到,redis事务执行时错误通常都是变成错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际生产环境中出现,所以没必要为redis开发事务回滚功能。

redis事务中涉及的命令和标志

  • WATCH:在执行MULTI命令之前设置监测的keys,并且在执行EXEC命令时能够判断是否至少有一个监测的key被修改,如果被修改就放弃执行事务,如果没被修改就清空WATCH的信息,执行缓存的命令列表。

  • UNWATCH:取消监测的key。

  • MULTI:开启redis事务。

  • EXEC:执行事务队列里面缓存的命令列表。

  • DISCARD:清空客户端事务队列里面的所有命令,并取消WATCH监测的所有key。

  • REDIS_MULTI:表示客户端处于事务状态。当客户端执行multi命令后便由非事务状态转变为事务状态。在非事务状态下命令是一个接一个按序执行的;而当客户端处于事务状态时,命令则以事务为单位执行,一次性执行事务队列中的所有命令。

  • REDIS_DIRTY_EXEC:表示EXEC无效状态。当客户端进入事务状态后,Redis等待接收一个或多个命令,并把它们放入命令队列中等待执行。如果某条命令在入队过程中发生错误则进入该状态,此时Redis将客户端的flags标识字段置为REDIS_DIRTY_EXEC。

  • REDIS_DIRTY_CAS:表示非安全状态,该状态是针对watch命令设置的,客户端可以在声明事务前使用watch命令对一个或多个key进行监视,如果在事务执行之前这些被监视的key被其他命令修改,则进入REDIS_DIRTY_CAS状态。因为此时将要执行事务所相关的key被修改,无法保证事务的原子性。REDIS_DIRTY_CAS状态下如果执行exec命令也会失败返回,即相当于该事务被取消。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值