1、Redis基本概念
1、NoSQL概述
NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,泛指非关系型的数据库。
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
特点:不遵循SQL标准、不支持ACID、支持海量数据的读写、支持对数据高可扩展性的场景。
2、Redis优势
Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。支持五种数据类型:string(字符串)、list(链表)、set(集合)、zset(有序集合)和hash(哈希类型)
3、Redis的安装与启动
yum install centos-release-scl scl-utils-build yum install -y devtoolset-8-toolchain scl enable devtoolset-8 bash gcc --version //需要先安装C语言的运行环境并检查测试gcc版本 // 去官网下载redis的tar.gz安装包并放到Linux的/opt目录 tar -zxvf redis-6.2.1.tar.gz cd redis-6.2.1 make make install //解压redis安装包并进入安装包目录执行make和make install命令进行安装 redis-server/myredis/redis.conf //后台启动Redis,并指定Redis启动时所“参考”的配置文件 redis-cli -p 6379 // 与客户的访问Redis服务器 daemonize no为yes redis-cli -p 6379 shutdown //关闭Redis服务器,指定端口是为了在Redis集群时也能关闭Redis
4、Redis数据库基本认识
默认16个数据库,类似数组下标从0开始,初始默认使用0号库,所有库同样密码
5、Redis的五大数据类型
-
Redis字符串String:是一键一值的,String类型是二进制安全的,它可以包含任何数据。比如jpg图片或者序列化的对象,一个字符串值最多可以是512M;
-
Redis列表List:一键多值,它是简单的字符串列表,它的下标标准是:从左边到右边的下标是从0开始,右边到左边的下标是从-1、-2、-3这样,底层是用双向链表实现的,对两端的操作性能很高,中间节点性能较差。特点是:值在键在,值光键亡;
-
Redis集合Set:功能与list类似,但set是可以自动排重的,Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1);
-
Redis哈希Hash:Redis hash 是一个键值对集合,相当于一个string类型的field和value的映射表,特别适合用于存储对象(一个key可对应多个field、一个field对应多个value、一个value可以是一个基本数据类型或一个对象),只要通过key+ field(属性标签)就可以操作对应属性数据了;Hash类型对应的数据结构是两种:当field-value长度较短且个数较少时,使用压缩列表ziplist,否则使用哈希表hashtable。
-
Redis有序集合Zset:也是一个没有重复元素的字符串集合,但每个成员都关联了一个评分(score),这个评分用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。它底层使用了两个数据结构:① hash(Redis中的hash),作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值;② 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
6、Redis的配置文件redis.conf
-
默认情况Redis服务器只能接受本机的客户端的访问请求,生产环境肯定是需要远程访问的,所以需要注释掉 “bind=127.0.0.1”;
-
将本机访问保护模式protected-mode设置为no;
-
设置本服务启动的端口号,默认是port 6379,在一台机器上启动多个Redis时需要保证其端口号不一致;
-
tcp-backlog n:在linux系统中控制tcp三次握手已完成连接队列的长度;在高并发系统中,需要设置一个较高的tcp-backlog来避免客户端连接速度慢的问题(这里没明白);已完成连接队列的长度也与操作系统中somaxconn有关,是取二者最小min(tcp-backlog,somaxconn),所以需要同时设置这两个参数;
-
网上有一篇文章对backlog 的含义是这样理解的:TCP建立连接时,有一个SYN队列(或半连接队列)和一个accept队列(或完整的连接队列)。处于SYN RECEIVED(正在建立连接)状态的连接被添加到SYN队列,并且当它们的状态改变为ESTABLISHED(成功建立连接)时,即当接收到3次握手中的ACK分组时,将它们移动到accept队列。 显而易见,accept系统调用只是简单地从SYN队列中取出连接。 在这种情况下,backlog参数表示SYN队列的大小;
-
timeout n:表示一个空闲的客户端维持多少秒会关闭,0表示关闭该功能,即永不关闭;
-
tcp-keepalive n:对访问客户端的一种心跳检测,每隔n秒检测一次,如果设置为0,则关闭检测功能,该功能的作用是检测客户端是否还“活着”,建议设置成60;
-
daemonize yes:设置是否允许后台启动Redis服务端,设置为yes;
-
requirepass foobared:作用是设置密码,默认是foobared。在命令行中设置的密码仅在本次服务器启动期间有效,在配置文件中的设置才是永久的,所以在在配置文件中把requirepass 的注释去掉;
-
maxclients:设置redis同时可以与多少个客户端进行连接,达到限制后,redis则会拒绝新的连接请求;
-
maxmemory:设置redis可以使用的内存量。一旦到达内存使用上限,redis将会通过maxmemory-policy规则来移除内部数据。必须设置,否则,将内存占满,造成服务器宕机;
-
maxmemory-policy:设置移除策略;
-
设置初始数据库的个数、设置pid文件的位置、设置日志级别、设置日志文件名称
2、SpringBoot整合Redis
1、简介:
Spring Data Redis 封装了在Java代码中对Redis的操作,它底层基于Jedis提供了一个高度封装的“RedisTemplate”类,创建这个类的对象就能对Redis进行各种操作;针对Jedis中大量API进行了归类封装,提供了很多Operations操作接口:ValueOperations简单字符串类型 SetOperations Set集合类型 ZSetOperations ZSet集合类型 HashOperations map数据类型 ListOperations list数据类型 。
2、说明:
-
问题:当我们通过RedisTemplate把数据保存后,去Redis数据库查看数据,发现它变成了二进制的形式;
原因:RedisTemplate在保存数据的时候,底层有一个序列化器在工作,他会将要保存的数据key-value按照一定的规则序列化之后再存储,RedisTemplate提供了六种序列化器(六个序列化子类),RedisTemplate默认使用的是JdkSerializationRedisSerializer,它序列化对象为二进制数据。可以通过配置类或调用方法redisTemplate.setKeySerializer()和redisTemplate.setValueSerializer()两个方法设置序列化方式,前者对当前工程都有效,后者只对当前的redisTemplate对象有效。
-
SpringDataRedi运行原理分析:redisTemplate就是在原生的jedis或其他操作redis的技术上做了一些封装,它屏蔽掉了细节,使我们的操作更加简单明了。
3、整合步骤
添加两个依赖
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring2.X集成redis所需common-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.6.0</version> </dependency>
在application.properties配置文件中配置redis
#Redis服务器地址 spring.redis.host=192.168.140.136 #Redis服务器连接端口 spring.redis.port=6379 #Redis数据库索引(默认为0),使用redis的哪个库 spring.redis.database= 0 #连接超时时间(毫秒) spring.redis.timeout=1800000 #连接池最大连接数(使用负值表示没有限制) spring.redis.lettuce.pool.max-active=20 #最大阻塞等待时间(负数表示没限制) spring.redis.lettuce.pool.max-wait=-1 #连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=5 #连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=0
添加redis配置类
@EnableCaching //开启缓存 @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setConnectionFactory(factory); //key序列化方式 template.setKeySerializer(redisSerializer); //value序列化 template.setValueSerializer(jackson2JsonRedisSerializer); //value hashmap序列化 template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题),过期时间600秒 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
实际使用:通过三个注解的操作Redis:在需要的控制器方法上添加缓存注解
@Cacheable ----对方法返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在(因为缓存有清除策略),则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。 @CachePut ----使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。 @CacheEvict —使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上。
测试实例:第一次访问时Redis没有缓存,所以会从MySQL数据库取数据,第二次访问时已经把数据存到了Redis,就直接从缓存中取了,不会再从MySQL中取数据。
4、RedisTemplate常用方法:
1、与key相关的操作
@Autowired //写好Redis配置类后,可以自动注入RedisTemplate对象 private RedisTemplate redisTemplate; //通用说明:① unit表示时间单位,取值有:TimeUnit.HOUR小时、TimeUnit.MINUTES分钟、TimeUnit.SECOND秒;multi--批量、Get--获取、Set--设置、Append--追加、IfAbsent--如果不存在、size--获取长度、Increment--自增、decrement--自减、delete--删除 redisTemplate.hasKey(key);//判断是否有key所对应的值,有则返回true,没有则返回false redisTemplate.opsForValue().get(key); //有则取出key值所对应的值 redisTemplate.delete(key); //删除单个key或多个key集合如Collection<K> keys redisTemplate.expire(key,timeout,unit);//设置过期时间为timeout分钟/小时/秒 redisTemplate.keys(pattern); //查根据正则表达式找匹配的key值,返回一个Set集合类型 redisTemplate.getExpire(key, unit); //返回剩余过期时间并且指定时间单位 redisTemplate.renameIfAbsent(oldKey, newKey); //修改redis中key的名称
2、String类型:
String类型 ValueOperations opsForValue = redisTemplate.opsForValue();//String类型操作对象 opsForValue.set(key, value); //设置当前的key以及value值 opsForValue.set(key, value, timeout, unit); //设置当前的key以及value值并且设置过期时间 opsForValue.setIfAbsent(key, value); //如果key不存在则存入key-value并返回true,否则返回false opsForValue.get(key, start, end); //返回key中字符串的子字符 opsForValue.multiGet(keys); //批量获取值,Collection<String> keys opsForValue.size(key); //获取字符串的长度 opsForValue().get(key, start, end); // 返回key中字符串 start~end 位置的子字符 opsForValue.append(key, value); //在原有的值基础上新增字符串到末尾 opsForValue.increment(key,increment); //给 key 对应的 value 值自增(负值则自减),如果该 key不存在,则新增该key opsForValue().size(key); // 获取字符串的长度 opsForValue().multiSet(valueMap); //Map<String, String> maps,map集合中的元素批量添加到Redis中
3、Hash类型
HashOperations<><><> opsForHash = redisTemplate.opsForHash();//也可以指定其泛型 opsForHash().putAll(key, maps); // Map<String, String> maps,放入多个field-value opsForHash.put(key, hashKey, value); //仅当hashKey不存在时新增hashMap值,注意value可以是一个对象类型的 opsForHash.get(key, field); //获取变量中的指定map键的值,没有返回null opsForHash.delete(key, fields); //删除一个或者多个hash表字段,值光键亡 opsForHash.hasKey(key, field); //查看hash表中指定字段是否存在 opsForHash.increment(key, field, increment); //给哈希表key中的指定字段的整数值加上增量 opsForHash.keys(key); //获取hash表中全部field(非value),返回值是set集合 opsForHash.values(key);//获取hash表中存在的所有的值(这次是value,非field),返回List<Object> opsForHash().entries(key); //获取变量中的键值对,返回值是map集合 opsForHash.scan(key, options); //匹配获取键值对,ScanOptions.NONE为获取全部键对
4、List集合类型
ListOperations opsForList = redisTemplate.opsForList(); opsForList.leftPush(key, value); //存储在list的头部,即在List最前面处 opsForList.leftPush(key, pivot, value);//在key中的值pivot左边添加元素value opsForList.leftPushAll(key, value);//把多个值存入key(value可以是多个值或一个Collection集合) opsForList.rightPush(key, value); //存储在list的尾部,即添加一个就把它放在最前面的索引处 opsForList.rightPushAll(key, value);//把多个值存入key右边、尾部 opsForList.range(key, start, end); //获取列表指定范围内的元素 opsForList.index(key, index); //通过索引获取列表中的元素,索引是左边0、1开始,右边-1、-2开始 opsForList.set(key, index, value); //设置指定索引处元素的值,索引取值同上 opsForList.size(key); //获取当前key的List列表长度 opsForList.leftPop(key);//移除并获取列表第一个元素 opsForList.rightPop(key); //移除并获取列表最后一个元素 opsForList.remove(key, index, value);//删除集合中值等于value的元素(index=0, 删除所有值等于value的元素; index>0, 从头部开始删除第一个值等于value的元素; index<0, 从尾部开始删除第一个值等于value的元素),因为List集合中元素是可重复的,所以需要指定删除第几个
4、Set集合类型
SetOperations opsForSet = redisTemplate.opsForSet(); opsForSet.add(key, values); //添加元素 opsForSet.remove(key, value1, value2...); //移除元素(单个值、多个值) opsForSet.size(key); //获取集合的大小 opsForSet.isMember(key, value); //判断集合是否包含value opsForSet.members(key); //获取集合中的所有元素 opsForSet.randomMembers(key, count); //随机获取集合中count个元素 opsForSet.intersectAndStore(key, otherKey, destKey); //key集合与otherKey集合的交集存储到destKey集合中(其中otherKey可以为单个值或者Collection集合) opsForSet.unionAndStore(key, otherKey, destKey); //key集合与otherKey集合的并集存储到destKey中(otherKey可以为单个key或多个key的集合) opsForSet.differenceAndStore(key, otherKey, destKey); //将key与otherKey的差集存储到destKey中(otherKeys可以为单个值或者集合) opsForSet.distinctRandomMembers(key, count); //获取多个key无序集合中的元素(去重),count表示个数
5、ZSet有序集合类型
ZSetOperations opsForZSet = redisTemplate.opsForZSet(); opsForZSet.add(key, value, score);//添加元素并指定score opsForZSet.remove(key, values); //删除对应的value,value可以为多个值 opsForZSet.incrementScore(key, value, delta); //增加元素的score值,并返回增加后的值 opsForZSet.rank(key, value); //返回元素在集合的排名,有序集合是按照元素的score值由小到大排列 opsForZSet.reverseRank(key, value); //返回元素在集合的排名,按元素的score值由大到小排列 opsForZSet.reverseRangeByScore(key, min, max);//查询集合中Score值在给定区间的元素 opsForZSet.reverseRangeByScoreWithScores(key, min, max);//返回值为元素及其score opsForZSet.count(key, min, max); //根据score值范围获取集合元素数量 opsForZSet.size(key); //获取集合的大小 opsForZSet.score(key, value); //获取集合中key、value元素对应的score值 opsForZSet.removeRange(key, start, end); //移除指定索引位置处的成员,索引规则与Set相同 opsForZSet.removeRangeByScore(key, min, max); //移除指定score范围的集合成员
3、Redis实战
1、Redis中的事务操作
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行;直到输入Exec后,Redis会将之前的命令队列中的命令依次执行而不被打扰。组队的过程中可以通过discard来放弃组队。
组队Multi中某个命令出现了错误,则整个队列都会被取消;如果执行阶段Exec某个命令出现错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。使用unwatch可以取消 对所有 key 的监视。
Redis事务三特性:单独的隔离操作 事务中的命令在执行的过程中,不会被其他客户端发送来的命令请求所打断;没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行;不保证原子性: 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
2、Redis持久化
Redis 提供了2个不同形式的持久化方式:RDB(Redis DataBase)和AOF(Append Of File)
1、RDB(Redis DataBase)
-
概念:在指定的时间间隔内将内存中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存里;
-
执行过程:Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
-
特点:有极高的性能(因为这个过程中主进程是不进行任何IO操作的)、对于数据恢复的完整性较差,最后一次持久化后的数据可能丢失;
-
持久化文件:通过redis.conf中配置文件中的dbfilename可以看到,默认持久化文件为dump.rdb,文件的保存路径默认为Redis启动时命令行所在的目录下。
-
持久化触发策略:通过配置文件中的bgsave可以看到,“bgsave seconds count”,表示在seconds秒内发生了多少次写操作就会触发持久化,一般会在多行写多个持久化策略;持久化会在后台异步进行, 同时还可以响应客户端请求(bgsave改为save时就只管保存,其它请求全部阻塞)。
2、AOF(Append Only File)
-
概念:以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,相当于重新执行一遍所有的操作;
-
持久化策略:AOF缓冲区根据AOF持久化策略[always每次写操作,everysec每一秒,no不主动进行同步]将所有写操作同步到磁盘的AOF文件中;AOF文件容量过大时,会重写压缩AOF文件容量,只保留可以恢复数据的最小指令集。
-
开启AOF:在redis.conf中配置持久化文件的名称,默认为 appendonly.aof,且AOF文件的保存路径,同RDB的路径一致;AOF和RDB同时开启,系统默认取AOF的数据。
3、Redis主从复制
1、概念:
主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。有利于读写分离,提高性能和容灾快速恢复。
2、实现:
开启多个redis服务器,在从机执行指令“slaveof IP port”使其成为哪个服务器的从机(当从服务器重启后就又变为了原来的样子---主服务器,就需要重新指定主服务器)。
3、主从复制原理
-
当从服务器连接上主服务器后,从服务器主动向主服务器发送sync进行数据同步主服务器收到消息后就把数据持久化到RDB文件,并把文件发送给从服务器;这里使用的就是全量复制(slave服务在接收到数据库文件数据后,将其存盘并加载到内存中)。
-
在这之后,每次主服务器进行写操作之后,就该主服务器主动的和从服务器进行数据同步,使用的就是增量复制(Master继续将新的所有收集到的修改命令依次传给slave,完成同步)。
4、三个常用的主从策略
三个策略,他们不是互斥的!!!应该是同时起作用的三种推荐策略。
-
一主二仆:复制方式就不说了;主服务器重启后仍是主服务器,从服务器不会“篡位”;而从服务器重启后需要重新“认主”;
-
薪火相传:其实就是分级管理。当从服务器数量太多,主服务器可能不能很好的同步数据;一个主服务器下有几个从服务器,而从服务器下也有数个属于它的从服务器,可以有效减轻master的写压力,去中心化降低风险;缺点是若其中一台服务器断了,从属于它的服务器就都联系不上了;它的主从策略和一主二仆相同。
-
反客为主:当一个master宕机后,在某个slave命令行界面使用 slaveof no one指令将其立刻升为master,其后面的slave不用做任何修改。
5、哨兵模式
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库;
-
实现方法:新建sentinel.conf文件并向其中写入内容“sentinel monitor mymaster 127.0.0.1 6379 1“其中mymaster为监控对象起的服务器名称,1为至少有多少个哨兵同意迁移的数量;然后执行”redis-sentinel /myredis/sentinel.conf“指令,redis-sentinel后跟的是sentinel.conf文件的绝对路径。
-
作用规则:当主机挂掉,哨兵就会在从机中选举新的主机,而原主机重启后就会变为从机;
-
选举规则:优先replica-priority最小的(默认是100,可以在slaveof的时候设置)----选择偏移量最大的(也就是获得原主机数据最全的)----选择runid最小的(每个redis实例启动后都会随机生成一个40位的runid);
4、Redis集群
1、概述
面临的两个问题:容量不够,redis如何进行扩容?并发写操作, redis如何分摊?
Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中;集群通过分区(partition)来提供一定程度的可用性(availability):即使集群中有一部分节点失效或者无法进行通讯, 集群中的其他节点也可以继续处理命令请求。
2、启动集群
启动各个Redis服务,然后用命令将这几个服务合并为一个集群即可。一个集群至少有三个节点,每个节点都有从服务器,
3、集群重要概念
1、插槽slot
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个;在redis-cli每次录入、查询键值时,集群使用公式 CRC16(key) % 16384 来计算键 key 的 CRC16 校验和 ,从而确定key 属于哪个槽;如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口;我们让集群中的每个节点负责处理一部分插槽。应当注意的是,一个插槽不只能插入一条数据。
2、故障恢复
如果集群中主节点故障,从节点就能变为主节点提供服务;
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续取决于redis配置文件中cluster-require-full-coverage的配置,若为yes,则整个集群一起挂掉;若为no,则该插槽不能继续使用,其他部分继续提供服务;
3、集群的优劣
实现扩容、分摊压力、无中心配置相对简单;多键的Redis事务是不被支持的、lua脚本不被支持、集群出现较晚,公司业务迁移难度大;
5、Redis应用问题
1、缓存穿透
1、问题描述
当大量的查询请求发送到服务器,而服务器不能在redis中找到数据,就得去MySQL中找,MySQL中也找不到,找不到就无法将查询结果缓存到redis,下次再有这个请求,还是得去MySQL查询,这样就严重增加了服务器压力;通常这样的情况表现为出现很多非正常的URL访问、Redis命中率严重下降、Redis还是正常运行,总之就是这时我们的缓存失去了作用,相当于被”击穿“了。
2、解决方案
-
对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟;没太懂这是啥意思???
-
进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
2、缓存击穿
1、问题描述
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
2、解决方案
-
预先设置并实时调整热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大并实时调整这些热门数据key的时长;
-
使用锁:当大量的线程去访问某个热门数据的时候,该数据在redis中不存在,那么最先进入redis访问的线程就给这个数据的访问加一个锁,然后该线程去MySQL中查数据、同步缓存,等成功后再释放锁,这时其他大量请求在缓存中就能拿到数据,而不必去MySQL中查找了。在加锁期间,其他线程遇到锁之后会短暂sleep再去查询。
3、缓存雪崩
1、问题描述
大量的key在redis中过期,此时若有大量相关的并发请求过来,这些请求发现缓存过期就都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
2、解决方案
-
构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等),但是操作麻烦,一般不建议。
-
将缓存失效时间分散开:我们可以在原有的失效时间基础上增加一个随机值,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
4、分布式锁
1、问题背景
随着业务发展,原单体单机部署的系统被演化成分布式集群系统后,原单机部署情况下的并发控制锁策略可能会在分布式集群系统中失效;
2、解决方案
使用分布式锁,可以是基于缓存(Redis等)或zookeeper等,redis性能最高,zookeeper稳定性最高。
在redis中通过SETEX key来设置锁(分布式锁就是用来存数据的???)。
SETNX key:当该key存在时,后续的相同操作就不能进行成功了,只有delete该key,才能表示释放锁,其他SETNX key操作才能成功。
可以知道,该锁是对所有分布式集群中的服务都有效的。
举例一个使用分布式锁的案例:使用方法比较巧妙。与Java的几种直接产生作用的锁不同,它使用setnx语句存入一个key,返回值作为要加锁的代码段的if条件在加锁的代码段中释放锁(删除该key即可),确实巧妙,我还在纳闷一个setnx怎么就能锁住别人了,原来是这样!!!
@GetMapping("testLock") public void testLock(){ Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");//1获取锁, if(lock){//2获取锁成功、查询num的值 Object value = redisTemplate.opsForValue().get("num"); if(StringUtils.isEmpty(value)){//2.1判断num为空return return; } int num = Integer.parseInt(value+"");//2.2有值就转成成int redisTemplate.opsForValue().set("num", ++num);//2.3把redis的num加1 redisTemplate.delete("lock");//2.4释放锁,del }else{//3获取锁失败、每隔0.1秒再获取 try { Thread.sleep(100); testLock(); } catch (InterruptedException e) { e.printStackTrace(); } } }
3、方案优化
-
设置锁的过期时间:如果拥有锁的线程卡住了,一直不结束,就会导致服务器性能严重下降,所以需要设置过期时间;
推荐使用setnx时顺便就指定过期时间;如果另外使用expire命令设置过期时间,由于redis事务操作缺乏原子性,很可能会导致过期时间设置失败;
-
使用UUID防止锁的误删:进程A拿到锁,在操作过程中意外卡住导致锁过期了,这时另外的进程B拿到了锁,B在执行过程中如果A突然恢复了,A的锁已经自动释放了,继续执行程序的话就会在程序中手动错误删除B的锁;
为防止这种情况,我们需要对每个进程的锁加以区分。我们本在设置锁key的value值时是随意设置的,而这里我们就要把value设置为一个UUID随机数(String uuid = UUID.randomUUID().toString();),然后在设置锁setnx的时候就把uuid作为value值;当需要删除锁时,比较uuid与该key的value值是否一致,一致才能删除,否则不能删除。
-
LUA保证原子性:在上一步使用UUID防止误删过程中,我们判断uuid是否等于锁的value与删除锁之间缺乏原子性,有可能在判断uuid一致后,正准备删除时,锁又刚好过期自动释放且被其他进程获取了,而原来的进程还是会继续执行删除锁的操作,这样也会造成误删;
为防止这种情况,我们使用LUA脚本来保证其一致性:使用LUA脚本来释放锁的代码如下:
/*使用lua脚本来锁*/ // 定义lua 脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; // 使用redis执行lua执行 DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(script); // 设置一下返回值类型 为Long // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String类型, 那么返回字符串与0 会发生错误。 redisScript.setResultType(Long.class); // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。 redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);