您的位置:澳门皇冠金沙网站 > 办公软件 > 浅析Redis分布式锁

浅析Redis分布式锁

2020-04-15 14:36
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.connection.jedis.JedisConnection;import org.springframework.stereotype.Service;import org.springframework.util.ReflectionUtils;import redis.clients.jedis.Jedis;import java.lang.reflect.Field;import java.util.Collections;@Servicepublic class RedisLock { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; @Autowired private RedisConnectionFactory connectionFactory; /** * 尝试获取分布式锁 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public boolean lock(String lockKey, String requestId, int expireTime) { Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis"); ReflectionUtils.makeAccessible(jedisField); Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection()); String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } /** * 释放分布式锁 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public boolean releaseLock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = getJedis().eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } public Jedis getJedis() { Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis"); ReflectionUtils.makeAccessible(jedisField); Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection()); return jedis; }}

2、Redis的数据过期及淘汰策略


Redis的数据过期策略和实现一直是让我非常困惑,Redis的文档也没有清晰的说明,下面结合源码一起分析下Redis的数据过期及淘汰策略。

第一版本:

  • Redis的线程结构,单线程结构、异步化组件、最佳线程数、性能瓶颈、阻塞场景
  • 过期和淘汰策略,令人困惑的expire、单线程中的定时器、淘汰策略和时机
  • 数据结构的选择,内部数据结构实现、hash/B+/LSM的特点和场景对比
  • 高可用和集群部署的方式

近期工作遇到需要业务场景如下,需要每天定时推送给另一系统一批数据,但是由于系统是集群部署的,会造成统一情况下任务争用的情况,所以需要增加分布式锁来保证一定时间范围内有一个Job来完成定时任务. 前期考虑的方案有采用ZooKeeper分布式任务,Quartz分布式任务调度,但是由于Zookeeper需要增加额外组件,Quartz需要增加表,并且项目中现在已经有Redis这一组件存在,所以考虑采用Redis分布式锁的情况来完成分布式任务抢占这一功能

3.1 字典 —— hashTable

字典在redis中主要有两个作用:

  • 实现数据库的键空间
  • 用作hash类型的一种底层实现

这里重点说下hash作为redis数据库的键空间的实现。

redis是一个键值对数据库,数据库中的键值对由字典来保存,这个字典被称为键空间。当用户添加一个键值对到redis(不论什么类型),redis都会把该键值对添加到键空间;当删除一个key时,则会把这个键值对从键空间中删除;当查询或者更新一个键时,都需要先在键空间中查询到这个键再做修改,所以键空间的实现以及其性能会直接影响到redis的整体性能

图片 1

redis 键空间

redis中键空间的结构如图所示,之所以采用hashTable作为键空间的实现结构,是因为hashTable在添加、删除、查找建的算法复杂度都是O(1),而redis作为一个缓存数据库,其性能是第一位的。

记录一下走过的弯路.

1.1 IO/业务单线程

准确的来说,Redis的单线程结构是指其主线程是单线程的,这里主线程包括IO事件的处理,以及IO对应的相关请求的业务处理,此外主线程还负责过期键的处理、复制协调、集群协调等等,这些除了IO事件之外的逻辑会被封装成周期性的任务由主线程周期性的处理。

正因为采用单线程的设计,对于客户端的所有读写请求,都由一个主线程串行地处理,因此多个客户端同时对一个键进行写操作不会有并发问题,避免了频繁的上下文切换和锁竞争。

并且在网络上使用epoll,利用epoll的非阻塞多路复用特性,不在IO上浪费时间。

第二版本:

1、Redis的线程结构


Redis在设计上最大的亮点是其单线程结构,并且还能提供极其强大的并发处理能力和丰富的数据结构,这点让我很激动也很是困惑的。

激动的是redis强大的并发处理能力,以及其丰富的api接口,让日常的业务需要可以更爽的完成。更让我惊叹的是单线程的设计导致redis的代码非常的小巧,整个源码大约5w行,而且不需要处理多线程引入的并发问题,整个代码理解起来也很顺畅。

困惑的是单线程的设计结构为什么能支持这么大的并发量,这一点和我们常规处理大并发的习惯性思维不同。一般在面对大并发的请求,首先想到的是用多个线程来处理,io线程和业务线程分开,业务线程使用线程池来避免频繁创建和销毁线程,即便是一次请求阻塞了也不会影响到其他请求。为什么redis会选择反其道而行之,这么做是否会局限redis的使用,在使用redis时有没有特别需要注意的点?

采用redis的 increament操作完成锁的抢占.但是释放锁时,是每个线程都可以删除redis中的key值. 并且initLock会降上一次的操作给覆盖掉,所以也废弃掉此方法

4.5 对比

产品 结构 扩/缩容 高可用 client
redis cluster 无中心 手动,使用redis-trib.rb进行节点迁移,迁移期间服务可用 集群master采用gossip协议检查master可用性,master采用redis sentinel作为group的高可用方案 直连redis实例,增加MOVED等重定向命令处理,client不支持事务、pipeline、mget等命令
codis 中心proxy 手动,使用RESTful api进行节点迁移,迁移期间服务可用 proxy检查master redis实例的可用性,某台master redis不可用时手动提升slave为master client连proxy,由proxy根据key决定请求到具体那台实例上,client不支持事务,但支持pipeline、mget等命令
tair configServer作为配置中心和监控dataServer,client直连dataServer 手动,添加新的dataServer到configServer, configServer根据一致性hash重做节点对照表发送到dataServer和client,dataServer根据节点对照表进行数据迁移 configServer通过心跳检测dataServer可用性,根据配置规则来决定是否采用备用节点 初始化和数据迁移时获取到节点对照表,client请求时根据key和节点对照表获取到key所在的dataServer机器,直连dataServer

最终版本:

3.2 hash or LSM or B+树

之所以特别关注redis中hashTable的实现,是想比较下场景存储实现方案中底层数据结构的实现。

平时的业务开发中,常常会根据业务场景需求的不同进行数据存储的技术选型,常见的存储实现有redis(缓存)、mysql、Hbase,它们面对的场景各不相同,各自的能力也不一样,其底层实现的数据结构也不一样,这是一个很有意思的点。

其实应该反过来想,正因为底层实现的数据结构的特点决定了其对外提供能力和适合应用场景的差别。redis、mysql(innodb)、hbase其采用的数据结构分别为hash、B+树、LSM树,它们各自的特性如下:

  • 哈希存储引擎是哈希表的持久化实现,支持增、删、改以及随机读取操作,但不支持顺序扫描,对应的存储系统为key-value存储系统。对于key-value的插入以及查询,哈希表的复杂度都是O(1),明显比树的操作O(n)快,如果不需要有序的遍历数据,哈希表就是最佳选举。
  • B树存储引擎是B树的持久化实现,不仅支持单条记录的增、删、读、改操作,还支持顺序扫描(B+树的叶子节点之间的指针),对应的存储系统就是关系数据库(Mysql等)。
  • LSM树(Log-Structured Merge Tree)存储引擎和B树存储引擎一样,同样支持增、删、读、改、顺序扫描操作。而且通过批量存储技术规避磁盘随机写入问题。当然凡事有利有弊,LSM树和B+树相比,LSM树牺牲了部分读性能,用来大幅提高写性能,hbase和levelDB的内部实现数据结构就是LSM树。
@Overridepublic T Long set(String key,T value, Long cacheSeconds) {if (value instanceof HashMap) {BoundHashOperations valueOperations = redisTemplate.boundHashOps(key);valueOperations.putAll((Map) value);valueOperations.expire(cacheSeconds, TimeUnit.SECONDS);}else{//使用map存储BoundHashOperations valueOperations = redisTemplate.boundHashOps(key);valueOperations.put(key, value);//秒valueOperations.expire(cacheSeconds, TimeUnit.SECONDS);}return null;}@Overridepublic void del(String key) {redisTemplate.delete(key);}

4.1 redis sentinel

redis2.8之前只提供了sentinel作为高可用方案而无分布式方案,但sentinel却和很多常用的高可用及读写分离方案有相似之处。

图片 2

redis sentinel

在sentinel系统中,服务器分为三种角色

  • master: 此套方案中master作为主节点,用于负责数据的读写sentinel架构中,并没有对数据进行分片处理,因此master只有一台。
  • slave: slave作为master从服务器,实时复制来自master的数据。必要时可设置读服务器,做到和master读写分离。
  • sentinel服务器: sentinel服务器用于监控数据服务器,并在master下线时选举slave作为新master。sentinel支持多台集群,并在每次选举master时先内部选出执行选举权的sentinel作为leader。

高可用

  • 主观下线:redis采用PING-PONG命令来维持心跳。任意sentinel服务器会向master、slave及其它sentinel服务器定时发送PING请求获取最新状态,若master返回无效响应或响应超时,则PING命令发送者会在本地将此master标注为“主观下线”。
  • 客观下线: 当sentinel将一个服务器判断为主观下线后,会向同样监视这一主服务器的其它sentinel进行询问,判断它们是否也认为master已进入了下线状态。当从其它sentinel获取了足够的主观下线判断后,Sentinel就会将此master标注为客观下线。
  • 选取sentinel leader:在master被判断为客观下线时,sentinel内部会先通过一定算法选取leader来执行后续的故障转移操作,leader是通过Raft算法进行选举。
  • 选出新的master:在salve中,挑选一个数据相对完整的slave提升为master,并将其他slave的复制目标改为新的master,选择mater的策略见Redis 的 Sentinel 文档

client

  • 以jedis为例,客户端在配置时只需要关心sentinel地址。sentinel服务器会定时与客户端通信以让客户端获取最新的master地址。如果slave配置了读写分离,则客户端也会从sentinel处获取slave地址用于读操作。

横向扩展

  • 很遗憾,由于sentinel不具备数据分片能力,因此能做的扩展就是增加slave节点,从而分担读压力。增加slave节点时,新节点会与master做一次数据完全同步,比较简单。
/** * 分布式锁 * @param range 锁的长度 允许有多少个请求抢占资源 * @param key * @return */ public boolean getLock(int range, String key) { ValueOperationsString, Integer valueOper1 = template.opsForValue(); return valueOper1.increment(key, 1) = range; } /** * 初始化锁, 设置等于0 * @param key * @param expireSeconds * @return */ public void initLock(String key, Long expireSeconds) { ValueOperationsString, Integer operations = template.opsForValue(); template.setKeySerializer(new GenericJackson2JsonRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); operations.set(key, 0, expireSeconds * 1000); } /** * 释放锁 * @param key */ public void releaseLock(String key) { ValueOperationsString, Integer operations = template.opsForValue(); template.setKeySerializer(new GenericJackson2JsonRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.delete(key); }

3、数据结构的选择


这里要说的数据结构不是redis对外支持的那几种,如果想了解可以去读redis的文档,这里关注下redis的内部实现数据结构。

redis对外支持string、list、set、hash、zset5种数据结构(最新版支持geo等数据结构,但不在讨论范围),内部实现以下数据结构来支撑:

  • 整数:REDIS_ENCODING_INT
  • 字符串:REDIS_ENCODING_RAW
  • 双端链表:REDIS_ENCODING_LINKEDLIST
  • 跳跃表:REDIS_ENCODING_SKIPLIST
  • 压缩列表:REDIS_ENCODING_ZIPLIST
  • 字典:REDIS_ENCODING_HT
  • 整数集合:REDIS_ENCODING_INTSET

内部数据结构和外部数据结构的对应关系如下:

图片 3

数据类型映射

采用set 和 del 完成锁的占用与释放,后经测试得知,set不是线程安全,在并发情况下常常会导致数据不一致.

4.2 redis cluster

redis3.0之后提供了支持数据分片的集群方式。

图片 4

redis cluster

在redis-cluster中,服务器分为两种角色

  • master:用于处理读写请求,并承担了sentinel模型中sentinel服务器的作用。cluster中可存在多个master,每个master处理一部分分片数据并不重复
  • slave:用于实时复制来自master的数据。不提供读写服务,当master故障时被选举为新的master

数据分片

  • 算法:CRC16(key)&16383 。redis cluster并没有采用常见的一致性哈希作为分片策略。而是通过计算key的CRC-16校验和的值与上16383将数据平均分配于0至16383个slot之一,每个master负责一部分slot的数据,然后启动master时由外部指定master需要处理的slot范围。
  • 主备复制:redis cluster采用异步冗余备份,主节点不需要等到从节点对写入操作的确认。采用异步设计性能上不会有太大的影响,但是导致的结果是会有一定数据不一致的风险。
  • 请求重定向:client向某master请求数据时,master会首先计算此数据是否属于自己负责的slot,若不是,则将客户端重定向至相应的master进行读取。此处需要注意的是, redis为了性能考虑,使用重定向而不是代理的方式减少网络及服务器性能开销。

图片 5

redis cluster

高可用

  • 疑似下线:master定期向其他节点发送PING消息,若无效响应或超市未响应则在本地将此节点标记为疑似下线
  • 已下线: 与sentinel不同的是,判断为疑似下线后并不会立刻广播向其它节点求证。而是从master之间的心跳信息来获取节点下线状态。当半数以上的master认为此节点下线时,最先确定这一情况的master会广播一条FAIL消息,任意slave节点都会接到此消息。
  • 选取主节点:与sentinel选取leader类似,都采用了Raft算法,只不过这一职责合并到了Master节点中。同时将其它slave指向至新的master
  • 数据迁移: 新选举的master将会管理已故master的slot,而由于每个master和slave之间都做了数据实时复制,因此不存在数据再分片的麻烦(cassandra不存在slave和master之分,因此每挂掉一个节点都需要进行数据再分片)

client

  • redis cluster是去中心化结构,在client进行访问时不需要先经过一个中心服务器,这样固然可以简化代码的开发,节省一次网络转发的开销,但是会导致client在进行服务请求的时候无法定位到具体key所在节点。redis cluster采用了重定向策略,客户端请求一个节点,如果所请求的key不在该节点上,客户端需要判断返回的move或ask等指令,重定向请求到对应的节点,为此需要client端做适当的适配和修改。

横向扩展

  • 增加新的master时,需要重新制定每个master负责的slot的范围,已有master会将相应slot对应的数据复制到新的master节点。由于redis数据分片算法相对简单,因此横向扩展时,有可能会导致所有节点数据的重新分布,目前redis机器在重新分片时需要使用redis-trib.rb来进行。

1.3 最佳线程数 —— 单线程

这里讨论下Redis的最佳线程数。

关于服务器性能的思考中,可以看到一个应用系统的最佳线程数的计算公式:

图片 6

最佳线程数

其中Tic是指一次请求过程中cpu计算耗时,Tiw是指请求处理过程中的等待耗时,这里的等待耗时可能由io、锁导致。但是redis是纯内存数据库,没有io操作,所以其Tiw为0,那么redis的最佳线程数就很明显了,就是一个线程。

再来看看,线程数对系统性能的影响:

图片 7

线程数对系统性能的影响

从压测的结果来看,系统的性能表现并不是线程越多越好,而是会在起最佳线程数附件产生性能拐点,过多的线程带来的上下文切换会对系统的性能表现带来负面影响。

通过对系统最佳线程数的讨论,我们可以明确为什么Redis采用单线程依然可以达到超高的并发处理能力,因为所有的请求都在内存中完成,根本不需要去等待io,只要机器的cpu没有达到瓶颈,再多的请求也不怕。

但是cpu会成为Redis的性能瓶颈吗,而且在多核cpu流行的今天,只有一个线程,只用一个核多少让人感觉很浪费,万一cpu的单核计算能力成为系统的性能瓶颈,又没法利用cpu其他的计算能力,那不是得不偿失?

2.1 令人困惑的expire

Redis文档中对于过期key的处理方式的描述有两种:被动和主动方式。

当一些客户端尝试访问它时,key会被发现并主动的过期。
但是这是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间,并将过期的keys进行删除。
具体就是Redis每秒10次做的事情:

  • 测试随机的20个keys进行相关过期检测。
  • 删除所有已经过期的keys。
  • 如果有多于25%的keys过期,重复步奏1。

但是Redis的主线程是单线程,并没有一个专门的线程来负责定时对过期数据进行清理,Redis如何具体完成过期key的查找、定时任务如何设置、对过期keys删除的效果如何?Redis的文档并没有明确的说明,需要从源码中查找。

本文由澳门皇冠金沙网站发布于办公软件,转载请注明出处:浅析Redis分布式锁

关键词: