Redis官网 https://redis.io/ 最新版本6.2.6
Redis中文官网 http://www.redis.cn/ 不过中文官网的同步更新维护相对要滞后不少时间,但对于我们基础学习完成足够了
Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存和消息代理。Redis提供丰富的数据结构,如字符串、哈希、列表、集合、带范围查询、位图、超对数、地理空间索引和流的排序集。Redis具有内置的复制、Lua脚本、LRU驱逐、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster的自动分区提供高可用性。
计数器、分布式ID生成器、海量数据统计bitmap、会话缓存、分布式阻塞队列、分布式锁、热点数据、社交需求好友推荐、延迟队列(sortset)等。
Redis与Mysql的部分场景比较
#Redis单机源码安装非常简单的,先下载,提取和编译就可以拉起来使用,Redis单机一般用于开发和学习环境,生产使用的话一般都是使用Redis Sentinel或者Redis Cluster保证高可用性
wget https://download.redis.io/releases/redis-6.2.6.tar.gz
tar xzf redis-6.2.6.tar.gz
cd redis-6.2.6
make && make install
#在当前目录下有redis的配置文件redis.conf,先修改redis.conf中的daemonize值为yes让redis以后台程序方式运行
redis-server redis.conf
#使用redis自带的客户端工具redis-cli
redis-cli
#向redis写入一个key名hello,值为world
set hello world
#读取key名称为hello的值
get hello
#Redis默认配置是16个数据库,通常没有特殊指定连接操作的是0号库,可以通过select命令选择库的索引,比如可以选择1号库
select 1
我们这里采用在同一台上多个端口运行多个redis实例的伪集群安装方式(当然也可以采用之前学习的docker等容器化的方式部署redis集群),同样需要先安装redis,可参考上面单机安装步骤。
#创建集群目录,放置各集群实例的配置和数据,创建六个文件夹,分别以端口号命名7000 7001 7002 7003 7004 7005六个以端口号为名字的子目录, 稍后我们在将每个目录中运行一个 Redis 实例
mkdir rediscluster
cd rediscluster
mkdir 7000 7001 7002 7003 7004 7005
#并将redis.conf配置文件拷贝六个目录下conf文件夹中,修改六个redis.conf 最少配置内容,端口port的配置和目录文件夹名称一致,其他内容如数据文件目录dir、bind、密码等配置可以按照实际的情况需求进行修改
vi redis.conf
daemonize yes
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
#分别进入6个端口目录
cd 7000
#分别启动相应目录下配置文件的redis实例
redis-server redis.conf
#配置集群信息
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1
redis-cli --cluster create 192.168.50.36:7000 192.168.50.36:7001 192.168.50.36:7002 192.168.50.36:7003 192.168.50.36:7004 192.168.50.36:7005 --cluster-replicas 1
出现下面的信息则代表集群的信息已经配置成功
#通过客户端登录redis集群
redis-cli -c -p 7000
#和上面一样读取键值验证redis集群是否正常
官网提供非常详细信息可以查阅,对于常见命令如所有数据结构读写操作命令都是需要熟悉的
也可以通过官方提供客户端help命令查阅
Redis支持非常多种语言的运营,官方上列出54种编程语言库,待黄色星号的是对应编程语言推荐的客户端库
以我们Java开发技术栈来说,推荐使用Jedis(一个非常小和健全的Redis Java客户端)、Lettuce(先进的Redis客户端线程安全同步,异步,和反应使用。支持集群、哨兵、流水线和编解码器。后面有Lettuce官网和GitHub源码地址,目前很多整合框架如SpringBoot都是使用Lettuce库)、Redisson(基于Redis服务器的分布式协调和可扩展的Java数据结构,如封装redis的分布式锁)。后面有时间我们专门针对Lettuce、Redisson这两个库实战和原理做专门剖析。
从Lettuce官网上就可以简单示例,包括对于怎么连接单机版本和集群版本,当然实际上我们更多的是使用Spring与Redis整合作为开发方式。
#如使用Maven,pom.xml加入下面依赖
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.5.RELEASE</version>
</dependency>
#如使用Gradle,build.gradle加入下面依赖
dependencies {
compile 'io.lettuce:lettuce-core:6.1.5.RELEASE
}
import io.lettuce.core.*;
public class ConnectToRedis {
public static void main(String[] args) {
#Redis分为16个库,下面使用的是0号库
RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> syncCommands = connection.sync();
syncCommands.set("testkey", "test string value");
connection.close();
redisClient.shutdown();
}
}
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
public class ConnectToRedisCluster {
public static void main(String[] args) {
// Syntax: redis://[password@]host[:port]
// Syntax: redis://[username:password@]host[:port]
RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7000");
StatefulRedisClusterConnection<String, String> connection = redisClient.connect();
System.out.println("Connected to Redis");
connection.close();
redisClient.shutdown();
}
}
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息,客户端订阅到一个或多个频道,其他客户端发到这些频道的消息将会被推送到所有订阅的客户端;发布/订阅与key所在空间没有关系,它不会受任何级别的干扰,包括不同数据库索引, 发布在db 10,订阅可以在db 1。
#客户端订阅执行
SUBSCRIBE devchannel testchannel
PSUBSCRIBE *itxiaoshen blog*
#发布信息
PUBLISH testchannel hello
PUBLISH productchannel hello
PUBLISH devchannel hello
PUBLISH new.itxiaoshen hello
PUBLISH sports.itxiaoshen hello
PUBLISH sports.xiaoshen hello
PUBLISH blog.csdn hello
#查看所有通道列表
PUBSUB CHANNELS
Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务,客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。服务端处理命令,并将结果返回给客户端。
管道一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。而当执行的命令较多时,这样的一来一回的网络传输所消耗的时间被称为RTT(Round Trip Time),显而易见,如果可以将这些命令作为一个请求一次性发送给服务端,并一次性将结果返回客户端,会节约很多网络传输的消耗,可以大大提升响应时间。
大量 pipeline 应用场景可通过 Redis 脚本(Redis 版本 >= 2.6)得到更高效的处理,后者在服务器端执行大量工作。脚本的一大优势是可通过最小的延迟读写数据,让读、计算、写等操作变得非常快(pipeline 在这种情况下不能使用,因为客户端在写命令前需要读命令返回的结果)。 Redis 中的脚本本身也就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。
使用内置的 Lua 解释器,可以对 Lua(Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能) 脚本进行求值,Redis Lua脚本适合简单快速执行的业务,如果是复杂计算业务则会阻塞Redis server端的处理业务。
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
#使用了redis为lua内置的redis.call函数
EVAL "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 good_price 99.00 300
#SCRIPT LOAD将一个脚本装入脚本缓存,但并不立即运行它
SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;"
#在脚本被加入到缓存之后,在任何客户端通过EVALSHA命令,可以使用脚本的SHA1校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止
EVALSHA 6aeea4b3e96171ef835a78178fceadf1a5dbe345 1 good_stock 1000 600
#SCRIPT EXISTS根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
SCRIPT EXISTS 6aeea4b3e96171ef835a78178fceadf1a5dbe345
#SCRIPT FLUSH清除所有脚本缓存
SCRIPT FLUSH
#SCRIPT KILL杀死当前正在运行的脚本
创建mytest.lua脚本文件
--- 获取key
local key = KEYS[1]
--- 获取value
local val = KEYS[2]
--- 获取一个参数
local expire = ARGV[1]
--- 如果redis找不到这个key就去插入
if redis.call("get", key) == false then
--- 如果插入成功,就去设置过期值
if redis.call("set", key, val) then
--- 由于lua脚本接收到参数都会转为String,所以要转成数字类型才能比较
if tonumber(expire) > 0 then
--- 设置过期时间
redis.call("expire", key, expire)
end
return true
end
return false
else
return false
end
#执行mytest.lua脚本文件
redis-cli --eval mytest.lua myKey myValue , 100
Redis 事务可以一次执行多个命令,将一系列的预定义命令放入队列,执行时按照添加顺序执行,redis 的事务更像是批量执行指令,有两个重要的保证:
加入事务的命令只是暂时存放在队列中,只有在执行了 exec 指令后才会被执行
使用事务时可能会遇上以下两种错误:
为什么Redis不支持事务回滚?
多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,这些Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。
严格的说Redis的命令是原子性的,而事务是非原子性的,Redis WATCH命令可以让事务具有回滚的能力。Redis使用WATCH命令来决定事务是继续执行还是回滚,那就需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。在WATCH之后,MULTI之前执行UNWATCH,则事务正常提交。
Redisson GitHub分布式锁使用示例 https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁,实际使用中我们也会基于redisson和spring框架的整合
maven pom依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.3</version>
</dependency>
#配置
Config config = new Config();
config.useClusterServers()
// use "rediss://" for SSL connection
.addNodeAddress("redis://127.0.0.1:7181");
#创建Redisson的实例
RedissonClient redisson = Redisson.create(config);
简单锁的示例
RLock lock = redisson.getLock("myLock");
// traditional lock method
lock.lock();
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
简单红锁的使用示例
RReadWriteLock rwlock = redisson.getReadWriteLock("myLock");
RLock lock = rwlock.readLock();
// or
RLock lock = rwlock.writeLock();
// traditional lock method
lock.lock();
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
Distributed locks:用Redis实现分布式锁管理器,分布式锁在很多场景中是非常有用,官方提供一个使用Redis实现分布式锁的Redlock算法,这种实现比普通的单实例实现更安全,下面为各种语言基于Redlock算法实现分布式锁。
一句话概括一致性哈希:就是普通取模哈希算法的改良版,哈希函数计算方法不变,只不过是通过构建环状的 Hash 空间代替普通的线性 Hash 空间。
数据存储的位置是沿顺时针的方向找到的环上的第一个节点,数据倾斜和节点宕机都可能会导致缓存雪崩。虚拟节点,就是对原来单一的物理节点在哈希环上虚拟出几个它的分身节点,这些分身节点称为「虚拟节点」。打到分身节点上的数据实际上也是映射到分身对应的物理节点上,这样一个物理节点可以通过虚拟节点的方式均匀分散在哈希环的各个部分,解决了数据倾斜问题。
redis 集群(cluster)并没有使用一致性哈希,而是采用了哈希槽(slot)的这种概念。主要的原因是一致性哈希算法的节点分布基于圆环,无法很好的手动控制数据分布,比如一个节点失效,把数据转移到下一个节点,容易造成缓存雪崩,而采用hash槽+副本节点失效的时候从节点自动接替,不易造成雪崩。
redis cluster 包含了16384个哈希槽,集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽,也即是每个 key 通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配,集群中的每一个节点负责处理一部分哈希槽。
redis底层核心实现是一个双数组,hash 数组+链表,通过哈希冲突解决方法如链表法、再哈希。
type是约束api, object encoding是底层实现类型
数据类型:string、list、set、sortset、hash、hyperloglog、 stream 、geo
底层数据结构:哈希表、跳表、双向链表、压缩列表等
压缩列表: redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。
Redis扩容是使用两个哈希表分多次渐进式rehash和动态扩容机制。当used大于size扩容,排除场景包括持久化、lua事务阻塞,如果大于5size则直接扩容,翻倍扩容如4-8-16,2指数主要方便位运算,可以将取模转为位运算,采用头插法,当used<=size*0.1时候进行缩容;redis扩容采用渐进式rehash的方式,redis CRUD每操作一次rehash一次,每毫秒100个数组槽位。
Redis同步机制分为全量复制和增量复制。全同步是指slave启动时进行的初始化同步。 增量复制是指Redis运行过程中的修改同步。
过期策略
淘汰策略:Redis官方给的警告,当内存不足时,Redis会根据配置的缓存策略淘汰部分keys,以保证写入成功。当无淘汰策略时或没有找到适合淘汰的key时,Redis直接返回out of memory错误。
持久化机制
dump.rdb
的二进制文件中。你可以对 Redis 进行设置, 让它在“N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。appendonly yes
bgrewriteaof
完成的,不同的是当开启混合持久化时,fork出的子进程先将共享的内存副本全量的以RDB方式写入aof文件,然后在将aof_rewrite_buf
重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图在redis重启的时候,加载 aof 文件进行恢复数据:先加载 rdb 内容再加载剩余的 aof。混合持久化配置:
aof-use-rdb-preamble yes # yes:开启,no:关闭
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。Redis采用网络IO多路复用技术来保证在多连接的时候,系统的高吞吐量。多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
Redis采用单线程为何支持高并发?
Redis6实现的多线程,只是对网络IO读写处理做多线程处理,但是对命令行的操作仍然是单线程的。这样即加快了IO处理效率,又保证了原子性。
Redis客户端和服务端之间使用一种名为RESP(REdis Serialization Protocol)的二进制安全文本协议进行通信,属于请求-响应模型。
#用SET命令来举例说明RESP协议的格式。
SET mykey "Hello"
#实际发送的请求数据:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n
#实际收到的响应数据:
+OK\r\n
RESP设计的十分精巧,下面是一张完备的协议描述图。
缓存穿透
定义
解决方案:
缓存击穿
缓存雪崩
缓存预热
缓存降级
缓存更新
redis技术点非常多,本章主要对redis有一个全局的理解,后续有时间我们再深入理解redis内容