在实际的开拓过程中,多多少少都会涉及到缓存,而 Redis 常日来说是我们分布式缓存的最佳选择。Redis 也是我们熟知的 NoSQL(非关系性数据库)之一,虽然其不能完备的替代关系性数据库,但它可作为其良好的补充。本文就和大家一起理解一下 Redis,并实战在 Spring Boot 项目利用它。
Redis 简介Redis 是一个开源(BSD 容许)、内存存储的数据构造做事器,可用作数据库,高速缓存和行列步队代理。它支持字符串、哈希表、列表、凑集、有序凑集等数据类型。内置复制、Lua 脚本、LRU 收回、事务以及不同级别磁盘持久化功能,同时通过 Redis Sentinel 供应高可用,通过 Redis Cluster 供应自动分区。
微做事以及分布式被广泛利用后,Redis 的利用场景就越来越多了,这里我罗列了紧张的几种场景。

当然 Redis 的利用场景并不仅仅只有这么多,还有很多未列出的场景,如计数、排行榜等,可见 Redis 的强大。不过 Redis 说到底还是一个数据库(非关系型),那么我们还是有必要理解一下它支持存储的数据构造。
Redis 数据类型前面也提到过,Redis 支持字符串、哈希表、列表、凑集、有序凑集五种数据类型的存储。理解这五种数据构造非常主要,可以说如果吃透了这五种数据构造,你就节制了 Redis 运用知识的三分之一,下面我们就来逐一解析。
字符串(string)string 这种数据构造该当是我们最为常用的。在 Redis 中 string 表示的是一个可变的字节数组,我们初始化字符串的内容、可以拿到字符串的长度,可以获取 string 的子串,可以覆盖 string 的子串内容,可以追加子串。
图 1. Redis 的 string 类型数据构造
如上图所示,在 Redis 中我们初始化一个字符串时,会采取预分配冗余空间的办法来减少内存的频繁分配,如图 1 所示,实际分配的空间 capacity 一样平常要高于实际字符串长度 len。如果您看过 Java 的 ArrayList 源码相信会对此种模式很熟习。
列表(list)在 Redis 中列表 list 采取的存储构造是双向链表,由此可见其随机定位性能较差,比较适宜首位插入删除。像 Java 中的数组一样,Redis 中的列表支持通过下标访问,不同的是 Redis 还为列表供应了一种负下标,-1 表示倒数一个元素,-2 表示倒数第二个数,依此类推。综合列表首尾增删性能精良的特点,常日我们利用 rpush/rpop/lpush/lpop 四条指令将列表作为行列步队来利用。
图 2. List 类型数据构造
如上图所示,在列表元素较少的情形下会利用一块连续的内存存储,这个构造是 ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成 quicklist。由于普通的链表须要的附加指针空间太大,会比较摧残浪费蹂躏空间。比如这个列表里存的只是 int 类型的数据,构造上还须要两个额外的指针 prev 和 next。以是 Redis 将链表和 ziplist 结合起来组成了 quicklist。也便是将多个 ziplist 利用双向指针串起来利用。这样既知足了快速的插入删除性能,又不会涌现太大的空间冗余。
哈希表(hash)hash 与 Java 中的 HashMap 差不多,实现上采取二维构造,第一维是数组,第二维是链表。hash 的 key 与 value 都存储在链表中,而数组中存储的则是各个链表的表头。在检索时,首先打算 key 的 hashcode,然后通过 hashcode 定位到链表的表头,再遍历链表得到 value 值。可能您比较好奇为啥要用链表来存储 key 和 value,直接用 key 和 value 一对一存储不就可以了吗?实在是由于有些时候我们无法担保 hashcode 值的唯一,若两个不同的 key 产生了相同的 hashcode,我们须要一个链表在存储两对键值对,这便是所谓的 hash 碰撞。
凑集(set)熟习 Java 的同学该当知道 HashSet 的内部实现利用的是 HashMap,只不过所有的 value 都指向同一个工具。Redis 的 Set 构造也是一样,它的内部也利用 Hash 构造,所有的 value 都指向同一个内部值。
有序凑集(sorted set)有时也被称作 ZSet,是 Redis 中一个比较特殊的数据构造,在有序凑集中我们会给每个元素授予一个权重,其内部元素会按照权重进行排序,我们可以通过命令查询某个范围权重内的元素,这个特性在我们做一个排行榜的功能时可以说非常实用了。其底层的实现利用了两个数据构造, hash 和跳跃列表,hash 的浸染便是关联元素 value 和权重 score,保障元素 value 的唯一性,可以通过元素 value 找到相应的 score 值。跳跃列表的目的在于给元素 value 排序,根据 score 的范围获取元素列表。
在 Spring Boot 项目中利用 Redis准备事情开始在 Spring Boot 项目中利用 Redis 之前,我们还须要一些准备事情。
一台安装了 Redis 的机器或者虚拟机。一个创建好的 Spring Boot 项目。添加 Redis 依赖Spring Boot 官方已经为我们供应好了集成 Redis 的 Starter,我们只须要大略地在 pom.xml文件中添加如下代码即可。Spring Boot 的 Starter 给我们在项目依赖管理上供应了诸多便利,如果您想理解更多 Starter 的内容,可以访问 这篇文章。
清单 1. 添加 Redis 依赖
<!--SpringBoot 的 Redis 支持--><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>
Show moreShow more icon
添加完依赖之后,我们还须要配置 Redis 的地址等信息才能利用,在 application.properties 中添加如下配置即可。
清单 2. Spring Boot 中配置 Redis
spring.redis.host=192.168.142.132spring.redis.port=6379# Redis 数据库索引(默认为 0)spring.redis.database=0# Redis 做事器连接端口# Redis 做事器连接密码(默认为空)spring.redis.password=#连接池最大连接数(利用负值表示没有限定)spring.redis.jedis.pool.max-active=8# 连接池最大壅塞等待韶光(利用负值表示没有限定)spring.redis.jedis.pool.max-wait=-1# 连接池中的最大空闲连接spring.redis.jedis.pool.max-idle=8# 连接池中的最小空闲连接spring.redis.jedis.pool.min-idle=0# 连接超时时间(毫秒)spring.redis.timeout=0
Spring Boot 的 spring-boot-starter-data-redis 为 Redis 的干系操作供应了一个高度封装的 RedisTemplate 类,而且对每种类型的数据构造都进行了归类,将同一类型操作封装为 operation 接口。RedisTemplate 对五种数据构造分别定义了操作,如下所示:
操作字符串:redisTemplate.opsForValue()操作 Hash:redisTemplate.opsForHash()操作 List:redisTemplate.opsForList()操作 Set:redisTemplate.opsForSet()操作 ZSet:redisTemplate.opsForZSet()但是对付 string 类型的数据,Spring Boot 还专门供应了 StringRedisTemplate 类,而且官方也建议利用该类来操作 String 类型的数据。那么它和 RedisTemplate 又有啥差异呢?
RedisTemplate 是一个泛型类,而 StringRedisTemplate 不是,后者只能对键和值都为 String 类型的数据进行操作,而前者则可以操作任何类型。两者的数据是不共通的,StringRedisTemplate 只能管理 StringRedisTemplate 里面的数据,RedisTemplate 只能管理 RedisTemplate 中 的数据。RedisTemplate 的配置一个 Spring Boot 项目中,我们只须要掩护一个 RedisTemplate 工具和一个 StringRedisTemplate 工具就可以了。以是我们须要通过一个 Configuration 类来初始化这两个工具并且交由的 BeanFactory 管理。我们在 cn.itweknow.sbredis.config 下面新建了一个 RedisConfig 类,其内容如下所示:
清单 3. RedisTemplate 和 StringRedisTemplate 的配置
@Configuration@ConditionalOnClass(RedisOperations.class)@EnableConfigurationProperties(RedisProperties.class)@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; }}
操作字符串
StringRedisTempalte 在上面已经初始化好了,我们只须要在须要用到的地方通过 @AutoWired 表明注入就行。
设置值,对付设置值,我们可以利用 opsForValue().void set(K var1, V var2);@Testpublic void testSet() { stringRedisTemplate.opsForValue().set("test-string-value", "Hello Redis");}
2.获取值,与 set 方法相对付 StringRedisTemplate 还供应了 .opsForValue().get(Object var1) 方法来获取指定 key 对应的 value 值。
@Testpublic void testGet() { String value = stringRedisTemplate.opsForValue().get("test-string-value"); System.out.println(value);}
3.设置值的时候设置过期韶光。在设置缓存的时候,我们常日都会给他设置一个过期韶光,让其能够达到定时刷新的效果。StringRedisTemplate 供应了 void set(K var1, V var2, long var3, TimeUnit var5) 方法来达到设置过期韶光的目的,个中 var3 这个参数便是过期韶光的数值,而 TimeUnit 是个列举类型,我们用它来设置过期韶光的单位,是小时或是秒等等。
@Testpublic void testGet() { String value = stringRedisTemplate.opsForValue().get("test-string-value"); System.out.println(value);}
4.删除数据,我们同样可以通过 StringRedisTmeplate 来删除数据, Boolean delete(K key)方法供应了这个功能。
@Testpublic void testSetTimeOut() { stringRedisTemplate.opsForValue().set("test-string-key-time-out", "Hello Redis", 3, TimeUnit.HOURS);}
Show moreShow more icon如上面代码所示,我们保存了一个 key 为 test-string-key-time-out 的 String 类型的数据,而这条数据将会在 3 个小时之后被自动删除(失落效)。
操作数组
在 Redis 数据类型小节中,我们提到过我们常常利用 Redis 的 lpush/rpush/lpop/rpop 四条指令来实现一个行列步队。那么这四条指令在 RedisTemplate 中也有相应的实现。
leftPush(K key, V value),往 List 左侧插入一个元素,如 从左边往数组中 push 元素:@Testpublic void testDeleted() { stringRedisTemplate.delete("test-string-value");}
2.rightPush(K key, V value),往 List 右侧插入一个元素, 如从右边往数组中 push 元素:
@Testpublic void testRightPush() { redisTemplate.opsForList().rightPush("TestList", "TestRightPush");}
Show moreShow more icon实行完上面两个 Test 之后,我们可以利用 Redis 客户端工具 RedisDesktopManager 来查看 TestList 中的内容,如下图 (Push 之后 TestList 中的内容)所示:此时我们再一次实行 leftPush 方法,TestList 的内容就会变成下图(第二次实行 leftPush 之后的内容)所示:可以看到 `leftPush` 实际上是往数组的头部新增一个元素,那么 `rightPush` 便是往数组尾部插入一个元素。
3.leftPop(K key),从 List 左侧取出第一个元素,并移除, 如从数组头部获取并移除值:
@Testpublic void testLeftPop() { Object leftFirstElement = redisTemplate.opsForList().leftPop("TestList"); System.out.println(leftFirstElement);}
Show moreShow more icon实行上面的代码之后,您会看到掌握台会打印出 `TestLeftPush`,然后再去 `RedisDesktopManager` 中查看 TestList 的内容,如下图 (同数组顶端移除一个元素后)所示。您会创造数组中的第一个元素已经被移除了。
4.rightPop(K key),从 List 右侧取出第一个元素,并移除, 如从数组尾部获取并移除值:
@Testpublic void testRightPop() { Object rightFirstElement = redisTemplate.opsForList().rightPop("TestList"); System.out.println(rightFirstElement);}
操作 Hash
Redis 中的 Hash 数据构造实际上与 Java 中的 HashMap 是非常类似的,供应的 API 也很类似。下面我们就一起来看下 RedisTemplate 为 Hash 供应了哪些 API。
Hash 中新增元素。@Testpublic void testPut() { redisTemplate.opsForHash().put("TestHash", "FirstElement", "Hello,Redis hash."); Assert.assertTrue(redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"));}
判断指定 key 对应的 Hash 中是否存在指定的 map 键,利用用法可以见上方代码所示。获取指定 key 对应的 Hash 中指定键的值。
@Testpublic void testGet() { Object element = redisTemplate.opsForHash().get("TestHash", "FirstElement"); Assert.assertEquals("Hello,Redis hash.", element);}
Show moreShow more icon
3.删除指定 key 对应 Hash 中指定键的键值对。
@Testpublic void testDel() { redisTemplate.opsForHash().delete("TestHash", "FirstElement"); Assert.assertFalse(redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"));}
Show moreShow more icon
操作凑集
凑集很类似于 Java 中的 Set,RedisTemplate 也为其供应了丰富的 API。
向凑集中添加元素。@Testpublic void testAdd() { redisTemplate.opsForSet().add("TestSet", "e1", "e2", "e3"); long size = redisTemplate.opsForSet().size("TestSet"); Assert.assertEquals(3L, size);}
Show moreShow more icon
2.获取凑集中的元素。
@Testpublic void testGet() { Set<String> testSet = redisTemplate.opsForSet().members("TestSet"); System.out.println(testSet);}
Show moreShow more icon实行上面的代码后,掌握台输出的是[e1, e3, e2],当然您可能会看到其他结果,由于 Set 是无序的,并不是按照我们添加的顺序来排序的。
获取凑集的长度,在像凑集中添加元素的示例代码中展示了如何获取凑集长度。移除凑集中的元素。
@Testpublic void testRemove() { redisTemplate.opsForSet().remove("TestSet", "e1", "e2"); Set testSet = redisTemplate.opsForSet().members("TestSet"); Assert.assertEquals("e3", testSet.toArray()[0]);}
Show moreShow more icon
操作有序凑集
与 Set 不一样的地方是,ZSet 对付凑集中的每个元素都掩护了一个权重值,那么 RedisTemplate 供应了不少与这个权重值干系的 API。
add(K key, V value, double score)添加元素到变量中同时指定元素的分值。
range(K key, long start, long end)获取变量指定区间的元素。
rangeByLex(K key, RedisZSetCommands.Range range)用于获取知足费 score 的排序取值。这个排序只有在有相同分数的情形下才能利用,如果有不同的分数则返回值不愿定。
angeByLex(K key, RedisZSetCommands.Range range,
RedisZSetCommands.Limit limit)用于获取知足费 score 的设置下标开始的长度排序取值。
add(K key, Set<ZSetOperations.TypedTuple<V>> tuples)通过 TypedTuple 办法新增数据。
rangeByScore(K key, double min, double max)根据设置的 score 获取区间值。
rangeByScore(K key, double min, double max,long offset, long count)根据设置的 score 获取区间值从给定下标和给定长度获取终极值。
rangeWithScores(K key, long start, long end)获取 RedisZSetCommands.Tuples 的区间值。
以上只是大略的先容了一些最常用的 API,RedisTemplate 针对字符串、数组、Hash、凑集以及有序凑集还供应了很多 API,详细有哪些 API,大家可以参考 RedisTemplate 供应的 API 列表 这篇文章。
实现分布式锁上面基本列出了 RedisTemplate 和 StringRedisTemplate 两个类所供应的对 Redis 操作的干系 API,但是有些时候这些 API 并不能完成我们所有的需求,这个时候我们实在还可以在 Spring Boot 项目中直接与 Redis 交互来完成操作。比如,我们在实现分布式锁的时候实在便是利用了 RedisTemplate 的 execute 方法来实行 Lua 脚本来获取和开释锁的。
清单 4. 获取锁
Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>)connection -> connection.set(key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")), Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
清单 5. 开释锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; boolean unLockStat = stringRedisTemplate.execute((RedisCallback<Boolean>)connection -> connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1, key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8"))));
最近几年 Redis 一贯都是口试的热点话题,在口试的过程中相信大家都会被问到缓存与数据库同等性问题、缓存击穿、缓存雪崩以及缓存并发等问题。那么在文章的末了部分我们就一起来理解一下这几个问题。
缓存与数据库同等性问题对付既有数据库操作又有缓存操作的接口,一样平常分为两种实行顺序。
先操作数据库,再操作缓存。这种情形下如果数据库操作成功,缓存操作失落败就会导致缓存和数据库不一致。第二种情形便是先操作缓存再操作数据库,这种情形下如果缓存操作成功,数据库操作失落败也会导致数据库和缓存不一致。大部分情形下,我们的缓存理论上都是须要可以从数据库规复出来的,以是基本上采纳第一种顺序都是不会有问题的。针对那些必须担保数据库和缓存同等的情形,常日是不建议利用缓存的。
缓存击穿问题缓存击穿表示恶意用户频繁的仿照要求缓存中不存在的数据,甚至这些要求短韶光内直接落在了数据库上,导致数据库性能急剧低落,终极影响做事整体的性能。这个在实际项目很随意马虎碰着,如抢购活动、秒杀活动的接口 API 被大量的恶意用户刷,导致短韶光内数据库宕机。对付缓存击穿的问题,有以下几种办理方案,这里只做简要解释。
利用互斥锁排队。当从缓存中获取数据失落败时,给当前接口加上锁,从数据库中加载完数据并写入后再开释锁。若其它线程获取锁失落败,则等待一段韶光后重试。利用布隆过滤器。将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及 DB 挂掉。缓存雪崩问题在短韶光内有大量缓存失落效,如果这期间有大量的要求发生同样也有可能导致数据库发生宕机。在 Redis 机群的数据分布算法上如果利用的是传统的 hash 取模算法,在增加或者移除 Redis 节点的时候就会涌现大量的缓存临时失落效的环境。
像办理缓存穿透一样加锁排队。建立备份缓存,缓存 A 和缓存 B,A 设置超时时间,B 不设值超时时间,先从 A 读缓存,A 没有读 B,并且更新 A 缓存和 B 缓存。打算数据缓存节点的时候采取同等性 hash 算法,这样在节点数量发生改变时不会存在大量的缓存数据须要迁移的情形发生。缓存并发问题这里的并发指的是多个 Redis 的客户端同时 set 值引起的并发问题。比较有效的办理方案便是把 set 操作放在行列步队中使其串行化,必须得一个一个实行。
结束语在这篇文章中,我们理解了 Redis 的利用场景、Redis 的五种数据构造,以及如何在 Spring Boot 中利用 Redis,文章的末了还列举了几个口试过程中常常被问到的关于 Redis 问题以及其办理方案
来源:
https://www.yelcat.cc/index.php/archives/1012/