Redis是现在最受欢迎的NoSQL数据库之一,一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库。
redis是使用C语言开发,但C中并没有字符串类型,只能使用指针或符数组的形式表示一个字符串,所以redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现,有点类似于java的string的底层实现,只是redis中采用的是空间预分配的策略,SDS对象中包含三个属性:
一个列表结构可以有序地存储多个字符串,可以使用lpush lpop rpush rpop等操作命令进行列表的读取和插入相关操作。3.2版本中底层实现基于quicklist,quicklist可以简单理解是一个ziplist组成的双向链表。
ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。
实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。
quicklist可以参考这篇博客
redis的散列可以存储多个键 值 对之间的映射,散列存储的值既可以是字符串又可以是数字值,并且用户同样可以对散列存储的数字值执行自增操作或者自减操作。散列可以看作是一个文档或关系数据库里的一行。hash底层的数据结构实现有两种:
一种是ziplist,上面已经提到过。当存储的数据超过配置的阀值时就是转用hashtable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足以下两个条件时才会使用这种结构:
当键的个数小于hash-max-ziplist-entries(默认512)
当所有值都小于hash-max-ziplist-value(默认64)
另一种就是hashtable。这种结构的时间复杂度为O(1),但是会消耗比较多的内存空间。
redis的集合和列表都可以存储多个字符串,它们之间的不同和java的list与set类似,列表可以存储多个相同的字符串,而集合则通过使用散列表(hashtable)来保证自已存储的每个字符串都是各不相同的(这些散列表只有键,但没有与键相关联的值),redis中的集合是无序的。
有序集合和散列一样,都用于存储键值对:有序集合的键被称为成员(member),每个成员都是各不相同的。有序集合的值则被称为分值(score),分值必须为浮点数。有序集合是redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序访问元素的结构。它的存储方式也有两种:
有序列表使用skiplist保障有序性和访问查找性能,dict就用来存储元素信息,并且dict的访问时间复杂度为O(1)。
随着移动互联网的到来,我们面对的三大难题就是海量数据,高可用和低延时,在这种背景下,redis作为一个基于内存的缓存数据库,优势就很明显。而项目中使用redis最多的场景也是作为热点数据的缓存,比如秒杀场景中的商品信息和详情等,游戏中的用户信息等,以及各种预加载的资源等。
现在服务基本都是集群+微服务方式部署,进行水平和垂直的拆分,保证服务的高可用和解耦,这个时候操作共享资源的时候,比如购物车模块大家都进行某个商品的抢购,请求可能会分发到不同的节点上,但是总库存是有限的,而且需要保证一致性,防止超卖或者少卖,jdk的相关锁是满足不了需求的,这种场景下,本身单线程能够保证事务,并且执行命令响应较快的redis就是首先,尤其redisson本身实现了一套分布式锁的api,使用起来很方便。
比如登陆token,游戏场景中某些限时彩蛋,或者一定实效性的验证码,这些是有时效性的,可以借助redis的expire方法,设置一个过期时间,对应的就是资源的有效时间,过期后redis会自动删除。
大家应该都听过分布式唯一id的生成,比较主流的有雪花算法和使用 Redis +mysql来预生成和分配 id,这里就是利用Redis的原子操作 INCR 和 INCRBY 来实现。类似场景还有分布式限流布隆过滤器底层实现等。
针对排行榜问题,比如游戏中的战力排行榜,微博热搜榜,微信步数榜等,关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助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);
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。 又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。
这个在奶茶活动中有运用,就是利用set存储用户之间的点赞关联的,另外在点赞前判断是否点赞过就利用了sismember方法,当时这个接口的响应时间控制在10毫秒内,十分高效。
Redis的Java客户端很多,官方推荐和日常使用较多的有三种:Jedis、Redisson和lettuce。
引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
引入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);
Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty使用了非阻塞IO,5.x版本之后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的API。
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;