1 redis简介
1.1 什么是redis
Redis是现在最受欢迎的NoSQL数据库之一,一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库。
1.2 redis特性
- 基于内存运行,性能高效,网上资料显示单节点redis每秒可以执行10w条命令;
- Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行;
- key-value存储系统,同时支持丰富的数据结构,string,list,set,zset,hash等;
- 有丰富的内存优化和淘汰策略;
- 支持 RDB和aof两种策略的持久化操作;
- 支持主从同步配置,支持集群搭建。
2 redis数据结构
2.1 String–字符串
redis是使用C语言开发,但C中并没有字符串类型,只能使用指针或符数组的形式表示一个字符串,所以redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现,有点类似于java的string的底层实现,只是redis中采用的是空间预分配的策略,SDS对象中包含三个属性:
- Int len 表示此字符串的实际长度,buf中已经占有的长度
- Int free 根据len> 1MB ? 1mb : len,表示 buf中未使用的缓冲区长度
- Char buf[] 实际保存字符串数据的地方
2.2 List–列表
一个列表结构可以有序地存储多个字符串,可以使用lpush lpop rpush rpop等操作命令进行列表的读取和插入相关操作。3.2版本中底层实现基于quicklist,quicklist可以简单理解是一个ziplist组成的双向链表。
ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。
实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。
quicklist可以参考这篇博客
2.3 hash–哈希
redis的散列可以存储多个键 值 对之间的映射,散列存储的值既可以是字符串又可以是数字值,并且用户同样可以对散列存储的数字值执行自增操作或者自减操作。散列可以看作是一个文档或关系数据库里的一行。hash底层的数据结构实现有两种:
-
一种是ziplist,上面已经提到过。当存储的数据超过配置的阀值时就是转用hashtable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足以下两个条件时才会使用这种结构:
当键的个数小于hash-max-ziplist-entries(默认512)
当所有值都小于hash-max-ziplist-value(默认64) -
另一种就是hashtable。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。
2.4 Set–集合
redis的集合和列表都可以存储多个字符串,它们之间的不同和java的list与set类似,列表可以存储多个相同的字符串,而集合则通过使用散列表(hashtable)来保证自已存储的每个字符串都是各不相同的(这些散列表只有键,但没有与键相关联的值),redis中的集合是无序的。
2.5 zset–有序集合
有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的。有序集合的值则被称为分值(score),分值必须为浮点数。有序集合是redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序访问元素的结构。它的存储方式也有两种:
- 一种是ziplist结构,与上面的hash中的ziplist类似,member和score顺序存放并按score的顺序排列
- 另一种是skiplist与dict的结合。
skiplist是一种跳跃表结构,用于有序集合中快速查找,大多数情况下它的效率与平衡树差不多,但比平衡树实现简单。redis的作者对普通的跳跃表进行了修改,包括添加span\tail\backward指针、score的值可重复这些设计,从而实现排序功能和反向遍历的功能。一般跳跃表的实现,主要包含以下几个部分: - 表头(head):指向头节点
- 表尾(tail):指向尾节点
- 节点(node):实际保存的元素节点,每个节点可以有多层,层数是在创建此节点的时候随机生成的一个数值,而且每一层都是一个指向后面某个节点的指针。
- 层(level):目前表内节点的最大层数
- 长度(length):节点的数量。
有序列表使用skiplist保障有序性和访问查找性能,dict就用来存储元素信息,并且dict的访问时间复杂度为O(1)。
2.6 3种高级数据结构
- bitmap,可以理解为redis实现的BloomFilter(布隆过滤器),bitmap并不是一种真实的数据结构,它本质上是String数据结构,只不过操作的粒度变成了位,即bit。因为String类型最大长度为512MB,所以bitmap最多可以存储2^32个bit;
- Geo本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET),并且使用GeoHash技术进行填充。Redis中将经纬度使用52位的整数进行编码,放进zset中,score就是GeoHash的52位整数值。在使用Redis进行Geo查询时,其内部对应的操作其实就是zset(skiplist)的操作。通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标。总之,Redis中处理这些地理位置坐标点的思想是:二维平面坐标点 --> 一维整数编码值 --> zset(score为编码值) --> zrangebyrank(获取score相近的元素)、zrangebyscore --> 通过score(整数编码值)反解坐标点 --> 附近点的地理位置坐标;
- Streams,这是Redis5.0引入的全新数据结构,用一句话概括Streams就是Redis实现的内存版kafka。而且,Streams也有Consumer Groups的概念。通过Redis源码中对stream的定义我们可知,streams底层的数据结构是Radix Tree(基数树),Radix Tree事实上就几乎相同是传统的二叉树。仅仅是在寻找方式上,以一个unsigned int类型数为例,利用这个数的每个比特位作为树节点的推断。能够这样说,比方一个数10001010101010110101010,那么依照Radix 树的插入就是在根节点,假设遇到0,就指向左节点,假设遇到1就指向右节点,在插入过程中构造树节点,在删除过程中删除树节点。
3 redis使用场景
3.1 热点数据的缓存
随着移动互联网的到来,我们面对的三大难题就是海量数据,高可用和低延时,在这种背景下,redis作为一个基于内存的缓存数据库,优势就很明显。而项目中使用redis最多的场景也是作为热点数据的缓存,比如秒杀场景中的商品信息和详情等,游戏中的用户信息等,以及各种预加载的资源等。
3.2 分布式锁
现在服务基本都是集群+微服务方式部署,进行水平和垂直的拆分,保证服务的高可用和解耦,这个时候操作共享资源的时候,比如购物车模块大家都进行某个商品的抢购,请求可能会分发到不同的节点上,但是总库存是有限的,而且需要保证一致性,防止超卖或者少卖,jdk的相关锁是满足不了需求的,这种场景下,本身单线程能够保证事务,并且执行命令响应较快的redis就是首先,尤其redisson本身实现了一套分布式锁的api,使用起来很方便。
3.3 限时业务的运用
比如登陆token,游戏场景中某些限时彩蛋,或者一定实效性的验证码,这些是有时效性的,可以借助redis的expire方法,设置一个过期时间,对应的就是资源的有效时间,过期后redis会自动删除。
3.4 计算器相关处理
大家应该都听过分布式唯一id的生成,比较主流的有雪花算法和使用 Redis +mysql来预生成和分配 id,这里就是利用Redis的原子操作 INCR 和 INCRBY 来实现。类似场景还有分布式限流布隆过滤器底层实现等。
3.5 排行榜相关问题
针对排行榜问题,比如游戏中的战力排行榜,微博热搜榜,微信步数榜等,关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。
//将玩家对应的数据放入zset中,第一个参数是key,第二个参数是值,第三个是存储的用户信息
jedis.zadd(Constants.USER_RANK, userScore.getScore(), value);
//给对应的value的数据增加number的值
jedis.zincrby(Constants.SALES_LIST, number, value);
// 按照用户分数多少排行,取出前五名
jedis.zrevrangeWithScores(Constants.USER_RANK, 0, 4);
3.6 点赞、好友等相互关系的存储
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。 又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
这个在奶茶活动中有运用,就是利用set存储用户之间的点赞关联的,另外在点赞前判断是否点赞过就利用了sismember方法,当时这个接口的响应时间控制在10毫秒内,十分高效。
4 redis java客户端
Redis的Java客户端很多,官方推荐和日常使用较多的有三种:Jedis、Redisson和lettuce。
4.1 Jedis
4.1.1 Jedis特点
- 轻量,简洁,便于集成和改造
- 支持连接池
- 支持pipelining、事务、LUA Scripting、Redis Sentinel、Redis Cluster
- 不支持读写分离,需要自己实现
4.1.2 Jedis使用
引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
创建jedis链接
4.2 高级客户端Redisson
4.2.1 Redisson特点
- 基于Netty实现,采用非阻塞IO,性能高
- 支持异步请求
- 支持连接池
- 支持读写分离,支持读负载均衡,在主从复制和Redis Cluster架构下都可以使用
- 支持同步/异步/异步流/管道流方式连接
- 分布式锁和同步器
- 多样化数据序列化
- 分布式集合(map,Multimap,set,list等)
- 文档较丰富,有中文文档
4.2.2 Redisson集成springboot
引入pom依赖
<!--redisson start -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--redisson end -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.3</version>
</dependency>
编写配置文件
server:
port: 1006 # 服务监听端口
spring:
application:
name: metadata-manage
profiles:
active: dev # 程序运行环境,决定了使用哪个配置文件
redis:
redisson:
config: classpath:config/redis/redisson-${spring.profiles.active}.yml
redisson-dev.yml
singleServerConfig:
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
pingTimeout: 1000
# 连接超时,单位:毫秒
connectTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 10000
# 命令失败重试次数,如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。
# 如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。
retryAttempts: 3
# 命令重试发送时间间隔,单位:毫秒
retryInterval: 1500
# 重新连接时间间隔,单位:毫秒
reconnectionTimeout: 3000
# 执行失败最大次数
failedAttempts: 3
# 单个连接最大订阅数量
subscriptionsPerConnection: 5
# 客户端名称
clientName: null
# 节点地址
address: ${REDIS_URL}
# 密码
password: ${REDIS_PASSWORD}
# 发布和订阅连接的最小空闲连接数
subscriptionConnectionMinimumIdleSize: 1
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
# 最小空闲连接数
connectionMinimumIdleSize: 32
# 连接池大小
connectionPoolSize: 64
# 数据库编号
database: 1
# DNS监测时间间隔,单位:毫秒
dnsMonitoringInterval: 5000
# 线程池数量,默认值: 当前处理核数量 * 2
threads: 0
# Netty线程池数量,默认值: 当前处理核数量 * 2
nettyThreads: 0
# 编码
codec: !<org.redisson.codec.JsonJacksonCodec> { }
# 传输模式
transportMode: "NIO"
项目中使用redisson
@Resource
public RedissonClient redissonClient;
//操作分布式map
RMap<Object, Object> map = redissonClient.getMap(uuid);
4.2 springBoot自带客户端生菜(lettuce)
4.2.1 lettuce特点
Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty使用了非阻塞IO,5.x版本之后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的API。
- 支持Redis的新增命令ZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX;
- 支持通过Brave模块跟踪Redis命令执行;
- 支持Redis Streams;
- 支持异步的主从连接;
- 支持异步连接池;
- 新增命令最多执行一次模式(禁止自动重连);
- 全局命令超时设置(对异步和反应式命令也有效)。
4.2.2 lettuce集成springboot
springboot2.x开始默认集成lettuce,所以不需要额外引入lettuce的依赖包
maven依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce pool 缓存连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.3</version>
</dependency>
<!-- jackson json 优化缓存对象序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</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>
<scope>test</scope>
</dependency>
配置文件编写
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接超时时间(毫秒)
spring.redis.timeout=10000
# Lettuce
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=10000
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
# 关闭超时时间
spring.redis.lettuce.shutdown-timeout=100
项目中使用lettuce
@Autowired
private RedisTemplate redisTemplate;