文章目录
- 一、 Redis简介
- 二、 基于Docker安装Redis单机版
- 三、 Redis常用命令
- 1 Key操作
- 2 字符串值(String)(值的长度不超过512MB)
- 3 哈希表(Hash)
- 4 列表(List)
- 5 集合(Set)
- 6 有序集合(Sorted Set)
- 四、 Redis持久化策略
- 1 RDB
- 2 AOF
- 五、 Redis主从复制
- 六、 哨兵(Sentinel)
- 七、Redis集群(Cluster)
- 八、 Jedis(了解)
- 九、 使用SpringBoot整合SpringDataRedis操作redis
- 十、 高并发下Redis可能存在的问题及解决方案
- 1 缓存击穿
- 2 缓存雪崩
- 3 缓存穿透(查询不存在数据)
- 4 边路缓存
- 5 Redis脑裂
- 6 Redis 缓存淘汰策略/当内存不足时如何回收数据/保证Redis中数据不出现内存溢出情况
一、 Redis简介
1 NoSQL简介
目前市场主流数据存储都是使用关系型数据库。每次操作关系型数据库时都是I/O操作,I/O操作是主要影响程序执行性能原因之一,连接数据库关闭数据库都是消耗性能的过程。尽量减少对数据库的操作,能够明显的提升程序运行效率。
针对上面的问题,市场上就出现了各种NoSQL(Not Only SQL,不仅仅可以使用关系型数据库)数据库,它们的宣传口号:不是什么样的场景都必须使用关系型数据库,一些特定的场景使用NoSQL数据库更好。
常见NoSQL数据库:
memcached :键值对,内存型数据库,所有数据都在内存中。
Redis:和Memcached类似,还具备持久化能力。
HBase:以列作为存储。
MongoDB:以Document做存储。
2 Redis简介
Redis是以Key-Value形式进行存储的NoSQL数据库。
Redis是使用C语言进行编写的。
平时操作的数据都在内存中,效率特高,读的效率110000/s,写81000/s,所以多把Redis当做缓存工具使用。
Redis以solt(槽)作为数据存储单元,每个槽中可以存储N多个键值对。Redis中固定具有16384。理论上可以实现一个槽是一个Redis。每个向Redis存储数据的key都会进行crc16算法得出一个值后对16384取余就是这个key存放的solt位置。
同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。
二、 基于Docker安装Redis单机版
1 拉取镜像
docker pull redis:6.2.1
2 创建并启动容器
docker run -d --name redis -p 6379:6379 --restart always redis:6.2.1
3 客户端测试
docker exec -it redis bash
docker exec -it redis redis-cli
在任意目录在输入redis-cli 即可进入redis命令行。
三、 Redis常用命令
Redis中数据是key-value形式,key为字符串类型,value可取类型如下:
String 字符串
Hash 哈希表
List 列表
Set 集合
Sorted Set 有序集合
Redis命令相关手册有很多,下面为其中比较好用的两个
https://www.redis.net.cn/order/
http://doc.redisfans.com/
1 Key操作
1.1 exists
判断key是否存在。
语法:exists key名称
返回值:存在返回数字,不存在返回0
1.2 expire
设置key的过期时间,单位秒
语法:expire key 秒数
返回值:成功返回1,失败返回0
1.3 ttl
查看key的剩余过期时间
语法:ttl key
返回值:返回剩余时间,如果不过期返回-1,如key不存在返回-2
1.4 del
根据key删除键值对。
语法:del key [key…]
返回值:被删除key的数量
1.5 keys
查看当前redis中的key数据
语法: keys 表达式 如: keys *
返回值:符合表达式的key列表
2 字符串值(String)(值的长度不超过512MB)
应⽤场景:
验证码、计数器、订单重复提交、⽤户登录信息、商品详情、分布式锁
2.1 set(mset批量设置)
设置指定key的值
语法:set key value
返回值:成功OK
2.2 get(mget批量获取)
获取指定key的值
语法:get key
返回值:key的值。不存在返回nil
2.3 setnx
当且仅当key不存在时才新增。恒新增,无修改功能。
语法:setnx key value
返回值:不存在时返回1,存在返回0
底层:
setnx具备分布式锁能力。在编写代码时如果调用setnx,时会对代码进行加锁。直到删除该key时会解锁。
setnx();// 加锁
// 代码
del();//解锁。
如果在并发访问时第一个线程setnx()时发现没有指定key会正常向下运行。其他线程在执行setnx()时发现有这个key就会等待,等待第一个线程删除key时才会继续向下执行。
常见的锁
锁:在Java中可以通过锁,让多线程执行时某个代码块或方法甚至类是线程安全的。通俗点说:一个线程访问,别的线程需要等待。
线程锁:同一个应用。多线程访问时添加的锁。synchronized(自动释放)或Lock(手动释放)
进程锁:不同进程(一个进程就是一个应用)需要访问同一个资源时,可以通过添加进程锁进行实现。
分布式锁:在分布式项目中不同项目访问同一个资源时,可以通过添加分布式锁保证线程安全。常见的分布式锁有两种:Redis的分布式锁和Zookeeper的分布式锁(通过调用Zookeeper的API给Zookeeper集群添加一个节点。如果节点能添加继续向下执行,执行结束删除该节点。如果其他线程发现该节点已经添加,会阻塞等待该节点删除才继续向下执行。)。
2.4 setex
设置key的存活时间,无论是否存在指定key都能新增,如果存在key覆盖旧值。同时必须指定过期时间。
语法:setex key seconds value
返回值:OK
2.5 incr
对key的值加一,返回新值
语法:incr key
返回值:新值
2.5 incrby
对key的值加increment,返回新值,若key不存在,操作之前key置为0
语法:incr key increment
返回值:新值
2.5 getset
对key设置新值,返回旧值(set返回为ok)
语法:getset key aa
返回值:key的旧值
3 哈希表(Hash)
应用场景:
购物⻋、⽤户个人信息、商品详情
3.1 hset
给key中field设置值。
语法:hset key field value
返回值:成功1,失败0
3.2 hget
获取key中某个field的值
语法:hget key field
返回值:返回field的内容
3.3 hmset
给key中多个filed设置值
语法:hmset key field value field value
返回值:成功OK
3.4 hmget
一次获取key中多个field的值
语法:hmget key field field
返回值:value列表
3.5 hkeys
获取key中所有的field的值
语法: hkeys key
返回值: field 列表
3.6 hvals
获取key中所有value的值
语法:hvals key
返回值:value列表
3.7 hgetall
获取所有field和value
语法:hgetall key
返回值:field和value交替显示列表
3.8 hdel
删除key中任意个field
语法:hdel key field field
返回值:成功删除field的数量,当删除key中所有的field,key自动删除。
4 列表(List)
应⽤场景:
简单队列、最新评论列表、非实时排行榜:定时计算榜单(如手机日销榜单)
4.1 rpush
向列表末尾中插入一个或多个值
语法;rpush key value value
返回值:列表长度
4.2 lrange
返回列表中指定区间内的值。可以使用-1代表列表末尾
语法:lrange list 0 -1
返回值:查询到的值
4.3 lpush
将一个或多个值插入到列表前面
语法:lpush key value value
返回值:列表长度
4.4 llen
获取列表长度
语法:llen key
返回值:列表长度
4.5 lrem
删除列表中元素。count为正数表示从左往右删除的数量。负数从右往左删除的数量。
语法:lrem key count value
返回值:删除数量。
4.6 lpop rpop
移除并获取最后一个元素,并返回该元素
语法:lpop key rpop key
返回值:删除元素。
4.6 brpop
移除并获取最后一个元素,没有元素会阻塞列表直到超时或发现有可弹出元素
语法:brpop key timeout
返回值:删除元素。
5 集合(Set)
应用场景:
去重
社交应用关注、粉丝、共同好友
统计网站的PV、uV、IP
大数据里面的用户画像标签集合
5.1 sadd
向集合中添加内容。不允许重复。
语法:sadd key value value value
返回值:本次命令新增数据个数
5.2 scard
返回集合元素数量
语法:scard key
返回值:集合长度
5.3 smembers
查看集合中元素内容
语法:smembers key
返回值:集合中元素
5.4 srem
删除集合中的元素
语法: srem key member [member…]
返回值:删除元素个数
6 有序集合(Sorted Set)
应用场景:
实时排行榜:商品热销榜、
体育类应用热门球队、积分榜优先级任务、队列
朋友圈文章点赞-取消,逻辑:用户只能点赞或取消,统计一篇文章被点赞了多少次,可以直接取里面有多少个成员
数据结构介绍:
使⽤HashMap+跳表skipList保证数据存储和有序
跳跃表性能堪⽐红⿊树,⽽且实现起来⽐红⿊树简单很多
有序集合中每个value都有一个分数(score),根据分数进行排序。
6.1 zadd
向有序集合中添加一个或者多个数据
语法:zadd key score value score value
返回值:新增的元素个数
6.2 zrange
返回区间内容,从小到大,withscores表示带有分数
语法:zrange key start stop [withscores]
返回值:值列表
6.3 zrem
删除集合内容
语法: zrem key member [member …]
返回值: 删除元素个数
6.4 zcard
获取有序集合成员数
语法: zcard key
返回值: 个数
6.5 zcount
获取有序集合指定区间内的成员数
语法: zcard key min max
返回值: 个数
6.6 zincrby
给指定元素增加increment分数
语法: zincrby key increment member
返回值: 增量后的分数
6.2 zrevange
返回区间内容,从大到小,withscores表示带有分数
语法:zrange key start stop [withscores]
返回值:值列表
四、 Redis持久化策略
Redis不仅仅是一个内存型数据库,还具备持久化能力。
1 RDB
rdb模式是默认模式,可以在指定的时间间隔内生成数据快照(snapshot),默认保存到dump.rdb文件中。当redis重启后会自动加载dump.rdb文件中内容到内存中。
用户可以使用SAVE(同步)或BGSAVE(异步)手动保存数据。
可以设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令,可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
例如:
save 900 1
save 300 10
save 60 10000
那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行
服务器在900秒之内,对数据库进行了至少1次修改
服务器在300秒之内,对数据库进行了至少10次修改
服务器在60秒之内,对数据库进行了至少10000次修改
1.1 优点
rdb文件是一个紧凑文件,直接使用rdb文件就可以还原数据。
数据保存会由一个子进程进行保存,不影响父进程。
恢复数据的效率要高于aof
1.2 缺点
每次保存点之间,因redis不可意料的关闭,可能会导致丢失数据。
由于每次保存数据都需要fork()子进程,在数据量比较大时可能会比较耗费性能。
2 AOF
AOF默认是关闭的,需要在配置文件中开启AOF。Redis支持AOF和RDB同时生效,如果同时存在,AOF优先级高于RDB(Redis重新启动时会使用AOF进行数据恢复)
监听执行的命令,如果发现执行了修改数据的操作,同时直接同步到数据库文件中。
2.1 优点
相对RDB数据更加安全。
2.2 缺点
相同数据集AOF要大于RDB。
相对RDB可能会慢一些。
2.3 开启办法
修改redis.conf中。
appendonly yes 开启aof
appendfilename 设置aof数据文件,名称随意。
# 默认no
appendonly yes
# aof文件名
appendfilename "appendonly.aof"
五、 Redis主从复制
Redis支持集群功能。为了保证单一节点可用性,redis支持主从复制功能。每个节点有N个复制品(replica),其中一个复制品是主(master),另外N-1个复制品是从(Slave),也就是说Redis支持一主多从。
一个主可有多个从,而一个从又可以看成主,它还可以有多个从。
1 主从优点
增加单一节点的健壮性,从而提升整个集群的稳定性。(Redis中当超过1/2节点不可用时,整个集群不可用)
从节点可以对主节点数据备份,提升容灾能力。
读写分离。在redis主从中,主节点一般用作写(具备读的能力),从节点只能读,利用这个特性实现读写分离,写用主,读用从。
2 基于Docker一主多从搭建
2.1 拉取redis镜像
# docker pull redis:6.2.1
2.2 创建并运行三个Docker容器
先停止单机版Redis。单机版Redis端口6379
三个容器分别占用系统的6479、6480、6481端口
# docker run --name redis1 -p 6479:6379 -v /opt/redis:/data -d redis:6.2.1
# docker run --name redis2 -p 6480:6379 -v /opt/redis:/data -d redis:6.2.1
# docker run --name redis3 -p 6481:6379 -v /opt/redis:/data -d redis:6.2.1
2.3 在从中指定主的ip和端口
设定redis1容器为主机(master)。redis2和redis3容器为从机(slave)
进入redis2容器内部设置主的ip和端口,连接从机,指定主机端口
# docker exec -it redis2 redis-cli
# slaveof 192.168.108.128 6379
# exit
进入redis2容器内部设置主的ip和端口
# docker exec -it redis3 redis-cli
# slaveof 192.168.108.128 6379
# exit
2.4 测试主从效果
进入redis1容器内部,新增key-value
# docker exec -it redis1 redis-cli
# set name "bjsxt"
# exit
分别进入redis2和redis3容器,查看是否有name键
# docker exec -it redis1 redis-cli
# get name
六、 哨兵(Sentinel)
在redis主从默认是只有主具备写的能力,而从只能读。如果主宕机,整个节点不具备写能力。但是如果这是让一个从变成主,整个节点就可以继续工作。即使之前的主恢复过来也当做这个节点的从即可。
Redis的哨兵就是帮助监控整个节点的,当节点主宕机等情况下,帮助重新选取主。
Redis中哨兵支持单哨兵和多哨兵。单哨兵是只要这个哨兵发现master宕机了,就直接选取另一个master。而多哨兵是根据我们设定,达到一定数量哨兵认为master宕机后才会进行重新选取主。我们以多哨兵演示。
1 没有哨兵下主从效果
只要杀掉主,整个节点无法在写数据,从身份不会变化,主的信息还是以前的信息。
七、Redis集群(Cluster)
1 集群原理
a) 集群搭建完成后由集群节点平分(不能平分时,前几个节点多一个槽)16384个槽。
b) 客户端可以访问集群中任意节点。所以在写代码时都是需要把集群中所有节点都配置上。
c) 当向集群中新增或查询一个键值对时,会对Key进行Crc16算法得出一个小于16384值,这个值就是放在哪个槽中,在判断槽在哪个节点上,然后就操作哪个节点。
集群:集群中所有节点都安装在不同服务器上。
伪集群:所有节点都安装在一台服务器上,通过不同端口号进行区分不同节点。
当集群中超过或等于1/2节点不可用时,整个集群不可用。为了搭建稳定集群,都采用奇数节点。
Redis每个节点都支持一主多从。会有哨兵监控主的状态。如果出现(配置文件中配置当多少个哨兵认为主失败时)哨兵发现主不可用时会进行投票,投票选举一个从当作主,如果后期主恢复了,主当作从加入节点。在搭建redis集群时,内置哨兵策略。
演示时:创建3个节点,每个节点搭建一主一从。一共需要有6个redis。
2 Redis集群安装步骤
2.1 新建配置模板文件
# cd /usr/local
# mkdir redis-cluster
# cd redis-cluster
# vim redis-cluster.tmpl
红色IP部分需要修改为自己的Linux虚拟机IP
port ${PORT}
protected-mode no
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 192.168.137.128
cluster-announce-port ${PORT}
cluster-announce-bus-port 1${PORT}
appendonly yes
2.2 使用Shell脚本创建6个目录
for port in `seq 7000 7005`; do \mkdir -p ./${port}/conf \&& PORT=${port} envsubst < ./redis-cluster.tmpl > ./${port}/conf/redis.conf \&& mkdir -p ./${port}/data; \
done
2.3 创建桥连网络
# docker network create redis-net
查看网络是否创建成功
# docker network ls
2.4 创建并启动6个容器
for port in `seq 7000 7005`; do \docker run -d -ti -p ${port}:${port} -p 1${port}:1${port} \-v /usr/local/redis-cluster/${port}/conf/redis.conf:/usr/local/etc/redis/redis.conf \-v /usr/local/redis-cluster/${port}/data:/data \--restart no --name redis-${port} --net redis-net \--sysctl net.core.somaxconn=1024 redis:5.0.5 redis-server /usr/local/etc/redis/redis.conf; \
done
2.5 查看6个容器ip及端口
# docker inspect redis-7000 redis-7001 redis-7002 redis-7003 redis-7004 redis-7005 | grep IPAddress
2.6 执行集群脚本
进入6个容器中任意一个。示例中以redis-7000举例
# docker exec -it redis-7000 bash
执行创建脚本命令。 --cluster-relicas 1表示每个主有1个从。
redis-cli --cluster create \
172.18.0.2:7000 \
172.18.0.3:7001 \
172.18.0.4:7002 \
172.18.0.5:7003 \
172.18.0.6:7004 \
172.18.0.7:7005 \
--cluster-replicas 1
输入后给出集群信息,输入yes后创建集群
2.7 验证集群
在任意Redis容器内部,进入Redis客户端工具。
示例中还是以Redis-7000举例。
# redis-cli -c -p 7000
八、 Jedis(了解)
Redis给Java语言提供了客户端API,称之为Jedis。
Jedis API和Redis 命令几乎是一样的。
例如:Redis对String值新增时set命令,Jedis中也是set方法。所以本课程中没有重点把所有方法进行演示,重要演示Jedis如何使用。
Jedis API特别简单,基本上都是创建对象调用方法即可。由于Jedis不具备把对象转换为字符串的能力,所以每次都需要借助Json转换工具进行转换,这个功能在Spring Data Redis中已经具备,推荐使用Spring Data Redis。
九、 使用SpringBoot整合SpringDataRedis操作redis
1 Spring Data简介
Spring Data是Spring公司的顶级项目,里面包含了N多个二级子项目,这些子项目都是相对独立的项目。每个子项目是对不同API的封装。
所有Spring Boot整合Spring Data xxxx的启动器都叫做spring-boot-starter-data-xxxx
Spring Data 好处很方便操作对象类型(基于POJO模型)。
只要是Spring Data 的子项目被Spring Boot整合后都会有一个XXXXTemplate示实例。
把Redis不同值得类型放到一个opsForXXX方法中。
opsForValue : String值(最常用),如果存储Java对象或Java中集合时就需要使用序列化器,进行序列化成JSON字符串。
opsForList : 列表List
opsForHash: 哈希表Hash
opsForZSet: 有序集合Sorted Set
opsForSet : 集合
2 Spring Data Redis序列化器介绍
经常需要向Redis中保存Java中Object或List等类型,这个时候就需要通过序列化器把Java中对象转换为字符串进行存储。
2.1 JdkSerializationRedisSerializer
是RedisTemplate类默认的序列化方式。JdkSerializationRedisSerializer使用JDK自带的序列化方式。要求被序列化的对象必须实现java.io.Serializable接口,而且存储的内容为二进制数据,这对开发者是不友好的。会出现虽然不影响使用,但是直接使用Redis客户端查询Redis中数据时前面出现乱码问题。
2.2 OxmSerializer
以字符串格式的xml存储。解析起来也比较复杂,效率也比较低。已经很少有人在使用该序列化器。
2.3 StringRedisSerializer
只能对String类型序列化操作。
2.4 GenericToStringSerializer
需要调用者给传递一个对象到字符串互转的Converter(转换器),使用比较麻烦。
2.5 Jackson2JsonRedisSerializer
该序列化器可以将对象自动转换为Json的形式存储,效率高且对调用者友好。
优点:
速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。
缺点:
此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。如果存储List等带有泛型的类型,此序列化器是无法识别泛型的,会直接把泛型固定设置为LinkedHashMap。
例如:存储时List , 取出时是List
2.6 GenericJackson2JsonRedisSerializer
与Jackson2JsonRedisSerializer功能相似。底层依然使用Jackson工具包。相比Jackson2JsonRedisSerializer多了_class列,列里面存储类(新增时类型)的全限定路径,从Redis取出时根据_class类型进行转换,解决了泛型问题。
该序列化器不需要指定对象类型信息(.class对象)使用Object作为默认类型。目前都使用这个序列化器。
3 代码步骤
基于单元测试演示
3.1 添加依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.0-M3</version>
</parent><dependencies><!-- 为了要在项目中jackson工具包 --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
</dependencies>
3.2 配置配置文件
spring.redis.host=localhost 默认值
spring.redis.port=6379 端口号默认值
如果连接Redis集群,不需要配置host,配置spring.redis.cluster.nodes,取值为redis集群所在ip:port,ip:port。由于word排版问题nodes后面取值没有和nodes在一行。
spring:redis:host: 192.168.52.133
# cluster:
# nodes: 192.168.52.133:7001,192.168.52.133:7002,192.168.52.133:7003,192.168.52.133:7004,192.168.52.133:7005,192.168.52.133:7006
3.3 编写配置类
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(factory);//配置key和value的序列化器。不适用默认的jdk序列化,使用json序列化redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());//如果使用hash数据类型。可以提供格外的序列化器redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());return redisTemplate;}
}
3.4 编写代码
package com.bjsxt;import com.bjsxt.pojo.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;@SpringBootTest
public class TestDataRedis {/*** Spring Data Redis提供的客户端对象。这个对象* 是spring-boot-starter-data-redis构建的。* 类型是RedisTemplate* 默认创建的客户端泛型是RedisTemplate<Object, Object>* 代表,当前客户端对象在访问Redis的时候,对key的类型约束是Object,对value的类型约束是Object** RedisTemplate中,提供了key和value的序列化器。* 默认提供的序列化器都是JDKSerializer,基于Serializable实现的序列化。* 数据读写的时候,RedisTemplate先把参数key和value,用序列化器转换成字节数组。* 在实现读写操作。** RedisTemplate是基于模板设计模式开发的类型。其中也包含很多其他的设计模式,包括不限于:* 工厂方法设计模式,构建器设计模式,模板方法设计模式,装饰模式等。* RedisTemplate基于工厂方法,为不同的数据类型,准备了不同的访问客户端。* 如:字符串操作,ValueOperations,通过redisTemplate.opsForValue()方法获取。* 如:hash操作,HashOperations,通过redisTemplate.opsForHash()获取。* RedisTemplate中也有大量的直接访问Redis服务器的方法,这些方法都是操作key的或者管理服务器的。* 如:删除键值对;查询有效时间;删除有效时间;设置有效时间等。*/@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Testpublic void testKeys(){redisTemplate.delete("user1");redisTemplate.expire("users", 1L, TimeUnit.MINUTES);System.out.println(redisTemplate.getExpire("users"));redisTemplate.persist("users");System.out.println(redisTemplate.getExpire("users"));}@Testpublic void testUsers(){List<User> user = new ArrayList<>();for(int i = 0; i < 3; i++){user.add(new User(i, "name"+i, "male"));}redisTemplate.opsForValue().set("users", user);}@Testpublic void testList(){redisTemplate.opsForList().rightPushAll("test-list", "1", "2", "3");System.out.println(redisTemplate.opsForList().range("test-list", 0, -1));}@Testpublic void testHash(){redisTemplate.opsForHash().put("test-hash1", "f1", "v1");System.out.println(redisTemplate.opsForHash().get("test-hash1", "f1"));}@Testpublic void testSetUser(){User user = new User(1, "张三", "男");ValueOperations<String, Object> valueOps = redisTemplate.opsForValue();valueOps.set("user3", user);System.out.println(valueOps.get("user3"));/*redisTemplate.opsForValue().set("user2", user);System.out.println(redisTemplate.opsForValue().get("user2"));*/}// 新增字符串数据@Testpublic void testSet(){redisTemplate.opsForValue().set("data-k2", "data-v2");Object value = redisTemplate.opsForValue().get("data-k2");System.out.println(value);System.out.println(redisTemplate.opsForValue().get("data-k1"));}@Testpublic void testClient(){System.out.println(redisTemplate);}
}
十、 高并发下Redis可能存在的问题及解决方案
1 缓存击穿
缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的⼀刻,同时有⼤量的请求,这些请求都会击穿到DB,造成瞬时DB请求量⼤、压⼒增⼤。和缓存雪崩的区别在于这⾥针对某⼀key缓存,后者则是很多key。
解决办法:
设置热点数据不过期,定时任务定时更新缓存
设置互斥锁
1.1 ReentrantLock(重入锁)
JDK对对于并发访问处理的内容都放入了java.util.concurrent中
ReentrantLock性能和synchronized没有区别的,但是API使用起来更加方便。
@SpringBootTest
public class MyTest {@Testpublic void test(){new Thread(){@Overridepublic void run() {test2("第一个线程111111");}}.start();new Thread(){@Overridepublic void run() {test2("第二个线程222222");}}.start();try {Thread.sleep(20000);} catch (InterruptedException e) {e.printStackTrace();}}ReentrantLock lock = new ReentrantLock();public void test2(String who){lock.lock();if(lock.isLocked()) {System.out.println("开始执行:" + who);try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("执行完:" + who);lock.unlock();}}
}
1.2 解决缓存击穿实例代码
只有在第一次访问时和Key过期时才会访问数据库。对于性能来说没有过大影响,因为平时都是直接访问redis。
private ReentrantLock lock = new ReentrantLock();
@Override
public Item selectByid(Integer id) {String key = "item:"+id;if(redisTemplate.hasKey(key)){return (Item) redisTemplate.opsForValue().get(key);}lock.lock();if(lock.isLocked()) {Item item = itemDubboService.selectById(id);// 由于设置了有效时间,就可能出现缓存击穿问题redisTemplate.opsForValue().set(key, item, 7, TimeUnit.DAYS);lock.unlock();return item;}// 如果加锁失败,为了保护数据库,直接返回nullreturn null;
}
SpringCache解决⽅案:
缓存的同步 ,sync 可以指示底层将缓存锁住,使只有⼀个线程可以进⼊计算,⽽其他线程堵塞,直到返回结果更新到缓存中
@Cacheable(value = {"product"},key ="#root.args[0]", cacheManager ="customCacheManager", sync=true)
2 缓存雪崩
缓存雪崩 (多个热点key都过期)⼤量的key设置了相同的过期时间,导致在缓存在同⼀时刻全部失效,造成瞬时DB请求量⼤、压⼒骤增,引起雪崩
预防:
存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣
设置热点数据永远不过期,定时任务定时更新
int seconds = random.nextInt(10000);
redisTemplate.opsForValue().set(key, item, 100+ seconds, TimeUnit.SECONDS);
SpringCache解决⽅案
1 设置差别的过时时间,⽐如CacheManager配置多个过期时间维度
2 配置⽂件 time-to-live 配置
spring:cache:type: redis#过时时间redis:time-to-live: 3600000
3 缓存穿透(查询不存在数据)
查询⼀个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id为“-1”不存在的数据如果从存储层查不到数据则不写⼊缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在⼤量查询不存在的数据,可能DB就挂掉了,这也是⿊客利⽤不存在的key频繁攻击应⽤的⼀种⽅式。
预防
接⼝层增加校验,数据合理性校验
缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置短点的过期时间,防⽌同个key被⼀直攻击
if(list==null){// key value 有效时间 时间单位redisTemplate.opsForValue().set(navKey,null,10, TimeUnit.MINUTES);
}else{redisTemplate.opsForValue().set(navKey,result,7,TimeUnit.DAYS);
}
SpringCache解决⽅案
空结果也缓存,默认不配置condition或者unless就⾏
spring:cache:type: redis#过时时间redis:time-to-live: 3600000# 开启前缀,默以为trueuse-key-prefix: true# 键的前缀,默认就是缓存名cacheNameskey-prefix: XD_CACHE# 是否缓存空结果,防⽌缓存穿透,默以为truecache-null-values: true
4 边路缓存
cache aside pattern 边路缓存问题。其实是一种指导思想,思想中包含:
- 查询的时候应该先查询缓存,如果缓存不存在,在查询数据库
- 修改缓存数据时,应先修改数据库,后修改缓存。
5 Redis脑裂
Redis脑裂主要是指因为一些网络原因导致Redis Master和Redis Slave和Sentinel集群处于不同的网络分区。Sentinel连接不上Master就会重新选择Master,此时就会出现两个不同Master,好像一个大脑分裂成两个一样。
Redis集群中不同节点存储不同的数据,脑裂会导致大量数据丢失。
解决Redis脑裂只需要在Redis配置文件中配置两个参数
min-slaves-to-write 3 //连接到master的最小slave数量
min-slaves-max-lag 10 //slave连接到master的最大延迟时间
6 Redis 缓存淘汰策略/当内存不足时如何回收数据/保证Redis中数据不出现内存溢出情况
Redis中数据都放入到内存中。如果没有淘汰策略将会导致内存中数据越来越多,最终导致内存溢出。在Redis5中内置了缓存淘汰策略。在配置文件中有如下配置
# maxmemory-policy noeviction 默认策略noevication
# maxmemory <bytes> 缓存最大阈值
# volatile-lru -> 在设置过期key集中选择使用数最小的。
# allkeys-lru -> 在所有key中选择使用最小的。
# volatile-lfu -> 在设置过期时间key集中采用lfu算法。
# allkeys-lfu -> 在所有key中采用lfu算法。
# volatile-random -> 在设置过期key集中随机删除。
# allkeys-random -> 在所有key中随机删除。
# volatile-ttl -> 在设置了过期时间key中删除最早过期时间的。
# noeviction -> 不删除key,超过时报错。
6.1 Lru和lfu算法
6.1.1 LRU
LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。LRU算法实现简单,运行时性能也良好,被广泛的使用在缓存/内存淘汰中。
• 新数据插入到链表头部
• 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
• 当链表满的时候,将链表尾部的数据丢弃
6.1.2 LFU
Least Frequently Used(最近最不经常使用)如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。
6.1.3 LRU和LFU的区别
LRU淘汰时淘汰的是链表最末尾的数据。而LFU是一段时间内访问次数最少的。
6.2 何时淘汰数据
- 消极方法(passive way):在读取数据时先判断是否过期,如果过期删除他。例如:get、hget、hmget等
- 积极方法(active way):周期性判断是否有失效内容,如果有就删除。
- 主动删除:当超过阈值时会删除。
在Redis中每次新增数据都会判断是否超过阈值。如果超过了,就会按照淘汰策略删除一些key。
6.3 每次删除多少
淘汰数据量和新增数据量进行判断。