Redis进阶教程
一、Redis简介
为什么需要用redis
在大数据互联网环境下,网民用户迅速增加,系统访问量也越来越高,用户的体验要求也越来越高,因为传统的关系数据库是基于磁盘iol来做数据的读取和写入,在性能上无法已经无法支持当前的用户需求,那么我们的系统为了给用户提供更高的用户的体验,需要具备以下要求:
- 响应速度
- 海量数据,支持高并发
- 成本控制
而NoSql出现了,代表作为redis,具有高可用\高可扩\高并发等特性,能够更低成本来实现更好的用户体验。
Redis(Remote Dictionary Server )是基于内存的非关系型分布式数据库,内部的数据结构为Key-Value
什么是NOSQL,与传统RDBMS(Relational Database Management System)的区别
(Structured Query Language)关系型数据库指的是使用关系模型(二维表格模型)来组织数据的数据库,由(行和列)二维表及其之间的联系所组成的一个数据组织。如mysql、oracle、sql server;
优势:
- 支持通用的SQL(结构化查询语言)语句
- 支持事务,遵循事务的acid,保障数据的一致性
- 支持复杂查询
劣势: - 表结构格式固定,扩展性差
- 相对与nosql数据库,写入效率低下
非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合,常见的数据类型有 键值、文档、图形、列簇
优势:
- 格式灵活
- 性能高:可基于硬盘或内存存储数据
- 扩展性强
- 成本低:开源软件,不收钱
劣势: - 学习成本高,每款nosql都可能有自己的语法
- 无事务控制,(mongodb除外)
- 结构复杂,很多sql不提供复杂查询
常用nosql举例
-
键值(Key-Value)数据库 Redis
适用的场景:
储存用户信息,比如会话、配置文件、参数、购物车等等。这些信息一般都和ID(键)挂钩,这种情景下键值数据库是个很好的选择。
缺点:Redis需要把数据存在内存中,这也大大限制了Redis可存储的数据量,不支持复杂查询
-
文档数据库 MongoDB(数据存储和管理服务)
在mongodb 4.2 中已支持分布式事务。MongoDB 通过二阶段提交的方式,实现在多个 分片 间发生的修改,要么同时发生,要么都不发生,保证事务的 ACID 特性。
适用的场景:
mongodb适用于需求变化快,表结构变更频繁的场景,字段类型可以随时修改
游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新
物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。
-
elasticsearch(数据检索服务)
elasticsearch更多适用于搜索和分析引擎,支持了复杂聚合查询,不适合做数据库,es会对所有字段做倒排索引
缺点:
ES需要在创建字段前要预先建立Mapping(关系数据库中的schema表定义),Mapping中包含每个字段的类型信息,ES需要根据Mapping为字段建立合适的索引。由于这个Mapping的存在,ES中的字段一但建立就不能再修改类型了。你建的数据表的某个字段忘了加全文搜索,你想临时加上,但是表已经建好并且已经有很多数据了,那这个时候只能删除重建mapping。
适用的场景:
ElasticSearch、Logstash 和 Kibana搭配来做日志的查询分析
-
列式数据库 hbase
适用的场景:
海量数据TB级数据存储,大数据查询,聊天消息,订单数据等
缺点:和redis一样,只能依赖rowkey做查询,不支持复杂查询
总结: -
对数据的读写要求极高,并数据规模不大,也不需要长期存储,选redis;
-
数据规模较大,数据的读性能要求很高,数据表的结构需要经常变,有时还需要做一些聚合查询,选MongoDB;
-
需要构造一个搜索引擎或者你想搞一个数据可视化平台,并且你的数据有一定的分析价值,选ElasticSearch;
-
需要存储海量数据,不知道数据规模将来会增长多么大,那么选HBase。
什么叫分布式数据库
分布式数据库系统通常使用较小的计算机系统,每台计算机可单独放在一个地方,每台计算机中都可能有DBMS的一份完整拷贝副本,或者部分拷贝副本,并具有自己局部的数据库,位于不同地点的许多计算机通过网络互相连接,共同组成一个完整的、全局的逻辑上集中、物理上分布的大型数据库。
1 高可扩展性:分布式数据库必须具有高可扩展性,能够动态地增添存储节点以实现存储容量的线性扩展,数据分布存储在不同的节点上,在逻辑上数据统一。
2 高并发性:分布式数据库必须及时响应大规模用户的读/写请求,能对海量数据进行随机读/写。
3 高可用性:分布式数据库必须提供容错机制,能够实现对数据的冗余备份,保证数据和服务的高度可靠性。
Redis(多主多从)具有数据分片、副本集从集中数据库转化为分布式数据库
CAP理论
Consistency (一致性)
所有副本集在同一时间的数据完全一致,这就是分布式数据库的一致性
Availability (可用性)
服务一直可用,而且是正常响应时间
Partition Tolerance (分区容错性):
分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务,分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体,如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转
大部分分布式系统都需要在C\A中做取舍,而分布式数据最常见的例子是读写分离
Base 理论
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结
- 基本可用(Basically Available)
假设系统,出现了不可预知的故障,但还是能用 -
- 响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。
-
- 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
- 软状态(Soft State)
允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时 - 最终一致性(Eventually Consistent)
不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性
而Redis集群遵从base理论,允许副本存在数据延时,但最终数据要完成同步,主从保障基本可用
redis特性
-
高并发
所有数据都存储在内存中,相比于磁盘操作,具有更高的性能,能支持更多的并发量,读11w/s,写 8w/s,基于io多路复用模型 -
高可用
redis支持集群模式,多主多从,假如系统某一个master宕机,从节点也会继续提供服务 -
高可扩
redis集群的所有槽点数为16384个,创建集群时会分配相应的槽点,假如redis需要横向扩展,那么只需要给新加入的集群分配相应的槽点,然后完成数据迁移,在扩容的时候,整个集群仍然提供服务 -
数据多样性
相比于memcache只有string数据类型,redis具有更多的数据类型,如list,set,zset等 -
可持久化
redis提供rdb和aof两种持久化方式,假如redis发生宕机,也会保存数据的副本,避免数据的大批量丢失,假如redis配置了save属性,那么根据配置的时间,redis会fork一个子进程来保存当前数据副本。 -
原子性,支持lua脚本
redis因为是采用单线程模型来处理客户端连接的,所以单命令的操作具有原子性,假如一次要执行多条命令,可以使用lua脚本完成。
二、Redis的安装与配置文件介绍
安装
在redis的目录下测试cluster的目录:
mkdir cluster
mkdir 710{1..6}
mkdir 710{1..6}/conf
mkdir 710{1..6}/data
mkdir 710{1..6}/log
创建conf文件,cd到conf文件,vim nodes-7101.conf
port 9001(每个节点的端口号)
daemonize yes
bind 0.0.0.0(一般设置为本地网卡ip,这里是绑定到所有的ipv4地址)
protect-mode yes
dir /opt/redis/redis-cluster/7101/data/(数据文件存放位置)
logfile "/opt/redis/redis-cluster/7101/log/7101.log"
pidfile /var/run/redis_7101.pid(pid 9001和port要对应)
save ""
#dbfilename不能配置为路径
dbfilename "dump-7101.rdb"
cluster-enabled yes(启动集群模式)
cluster-config-file nodes-7101.conf (集群配置文件)
cluster-node-timeout 15000
cluster-require-full-coverage no (集群不正常,也可以用)
#集群总线配置
cluster-announce-ip ip地址
# cluster-announce-tls-port 7101
cluster-announce-port 7101
cluster-announce-bus-port 17101
masterauth 123
requirepass 123
启动集群
redis-server ./7101/conf/nodes-7101.conf
redis-server ./7102/conf/nodes-7102.conf
redis-server ./7103/conf/nodes-7103.conf
redis-server ./7104/conf/nodes-7104.conf
redis-server ./7105/conf/nodes-7105.conf
redis-server ./7106/conf/nodes-7106.conf
集群互联
redis-cli --cluster create --cluster-replicas 1 ip地址:7101 ip地址:7102 ip地址:7103 ip地址:7104 ip地址:7105 ip地址:7106
查看集群状态
redis-cli -p 7101 cluster info
redis-cli -p 7101 cluster nodes
Redis压力测试
Redis自带一个性能测试工具:redis-benchmark。
系统的吞吐量是指系统的抗压、负载能力,指的是单位时间内处理的请求数量。
通常情况下,吞吐率用 “字节数/秒” 来衡量,也可以用 “请求数/秒”,“页面数/秒”,其实,不管是一个请求还是一个页面,本质都是网络上传输的数据,那么表示数据的单位就是字节数
影响因素(服务器配置、各类io)
QPS
Queries Per Second,每秒查询数,即是每秒能够响应的查询次数,注意这里的查询是指用户发出请求到服务器做出响应成功的次数,简单理解可以认为查询=请求request
TPS
Transactions Per Second ,每秒处理的事务数。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。
针对单接口而言,TPS可以认为是等价于QPS的,比如访问一个页面/index.html,是一个TPS,而访问/index.html页面可能请求了3次服务器比如css、js、index接口,产生了3个QPS。
响应时间RT
Response Time,简单理解为系统从输入到输出的时间间隔,宽泛的来说,代表从客户端发起请求到服务端接收到请求并响应所有数据的时间差。一般取平均响应时间。
一般会采用P99.9值,也就是99.9%用户耗时作为指标,意思就是1000个用户里面,999个用户的耗时上限,通过测量与优化该值,就可保证绝大多数用户的使用体验。 至于P99.99值,优化成本过高,而且服务响应由于网络波动、系统抖动等不能解决之情况,因此大多数时候都不考虑该指标。
[root@VM-0-3-centos ~]# redis-benchmark --help
Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests>] [-k <boolean>]
-h <hostname> Server hostname (default 127.0.0.1)
-p <port> Server port (default 6379)
-s <socket> Server socket (overrides host and port)
-a <password> Password for Redis Auth
--user <username> Used to send ACL style 'AUTH username pass'. Needs -a.//redis6以上版本可使用用户名加密码来控制访问权限
-c <clients> Number of parallel connections (default 50)
-n <requests> Total number of requests (default 100000)
-d <size> Data size of SET/GET value in bytes (default 3)
--dbnum <db> SELECT the specified db number (default 0)
--threads <num> Enable multi-thread mode.
--cluster Enable cluster mode.
--enable-tracking Send CLIENT TRACKING on before starting benchmark.
-k <boolean> 1=keep alive 0=reconnect (default 1)
-r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD,
random members and scores for ZADD.
Using this option the benchmark will expand the string __rand_int__
inside an argument with a 12 digits number in the specified range
from 0 to keyspacelen-1. The substitution changes every time a command
is executed. Default tests use this to hit random keys in the
specified range.
-P <numreq> Pipeline <numreq> requests. Default 1 (no pipeline).
-q Quiet. Just show query/sec values
--precision Number of decimal places to display in latency output (default 0)
--csv Output in CSV format
-l Loop. Run the tests forever
-t <tests> Only run the comma separated list of tests. The test
names are the same as the ones produced as output.
-I Idle mode. Just open N idle connections and wait.
--help Output this help and exit.
--version Output version and exit.
Examples:
Run the benchmark with the default configuration against 127.0.0.1:6379:
$ redis-benchmark
Use 20 parallel clients, for a total of 100k requests, against 192.168.1.1:
$ redis-benchmark -h 192.168.1.1 -p 6379 -n 100000 -c 20
Fill 127.0.0.1:6379 with about 1 million keys only using the SET test:
$ redis-benchmark -t set -n 1000000 -r 100000000
Benchmark 127.0.0.1:6379 for a few commands producing CSV output:
$ redis-benchmark -t ping,set,get -n 100000 --csv
Benchmark a specific command line:
$ redis-benchmark -r 10000 -n 10000 eval 'return redis.call("ping")' 0
Fill a list with 10000 random elements:
$ redis-benchmark -r 10000 -n 10000 lpush mylist __rand_int__
On user specified command lines __rand_int__ is replaced with a random integer
with a range of values selected by the -r option.
redis-benchmark -n 100000
====== SET ======
100000 requests completed in 1.40 seconds
50 parallel clients
3 bytes payload
keep alive: 1
host configuration "save":
host configuration "appendonly": no
multi-thread: no
Latency by percentile distribution:
0.000% <= 0.175 milliseconds (cumulative count 1)
50.000% <= 0.431 milliseconds (cumulative count 51820)
75.000% <= 0.559 milliseconds (cumulative count 75600)
87.500% <= 0.687 milliseconds (cumulative count 87833)
93.750% <= 0.767 milliseconds (cumulative count 93888)
96.875% <= 0.815 milliseconds (cumulative count 97247)
98.438% <= 0.847 milliseconds (cumulative count 98575)
99.219% <= 0.887 milliseconds (cumulative count 99246)
99.609% <= 0.959 milliseconds (cumulative count 99615)
99.805% <= 1.175 milliseconds (cumulative count 99805)
99.902% <= 1.543 milliseconds (cumulative count 99903)
99.951% <= 3.247 milliseconds (cumulative count 99952)
99.976% <= 3.679 milliseconds (cumulative count 99976)
99.988% <= 8.063 milliseconds (cumulative count 99988)
99.994% <= 8.103 milliseconds (cumulative count 99994)
99.997% <= 8.159 milliseconds (cumulative count 99997)
99.998% <= 8.183 milliseconds (cumulative count 99999)
99.999% <= 8.207 milliseconds (cumulative count 100000)
100.000% <= 8.207 milliseconds (cumulative count 100000)
Cumulative distribution of latencies:
0.000% <= 0.103 milliseconds (cumulative count 0)
0.002% <= 0.207 milliseconds (cumulative count 2)
9.121% <= 0.303 milliseconds (cumulative count 9121)
45.394% <= 0.407 milliseconds (cumulative count 45394)
66.334% <= 0.503 milliseconds (cumulative count 66334)
81.275% <= 0.607 milliseconds (cumulative count 81275)
89.080% <= 0.703 milliseconds (cumulative count 89080)
96.683% <= 0.807 milliseconds (cumulative count 96683)
99.352% <= 0.903 milliseconds (cumulative count 99352)
99.718% <= 1.007 milliseconds (cumulative count 99718)
99.781% <= 1.103 milliseconds (cumulative count 99781)
99.819% <= 1.207 milliseconds (cumulative count 99819)
99.860% <= 1.303 milliseconds (cumulative count 99860)
99.882% <= 1.407 milliseconds (cumulative count 99882)
99.898% <= 1.503 milliseconds (cumulative count 99898)
99.912% <= 1.607 milliseconds (cumulative count 99912)
99.922% <= 1.703 milliseconds (cumulative count 99922)
99.930% <= 1.807 milliseconds (cumulative count 99930)
99.942% <= 3.103 milliseconds (cumulative count 99942)
99.980% <= 4.103 milliseconds (cumulative count 99980)
99.994% <= 8.103 milliseconds (cumulative count 99994)
100.000% <= 9.103 milliseconds (cumulative count 100000)
Summary:
throughput summary: 71326.68 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.475 0.168 0.431 0.791 0.871 8.207
三、Redis基本数据类型与命令介绍
中文命令网站:https://www.redis.net.cn/order/
五种基本数据类型
- String类型:String数据类型是简单的key-value类型,可存储String,Integer和Bit类型字符串,String类型是redis最基本的数据类型,一个redis中字符串value最多可以是512M。
- hash类型:Redis中的hash是一个String类型的filed和value的映射表,每个hash可以存放2^23-1个键值对,类似于Java中的Map<String,Object>。
- list类型:Redis中的list是存储String类型元素的双向链表,可以支持通过push和pop操作实现从列表的头部或者尾部添加或则删除元素,可以当作栈也可以当作队列来用。
- set类型:Redis的Set是String类型的无序集合,集合中不允许出现重复的元素。
- zset类型:Redis zset 和 set 一样,也是String类型元素的集合,且不允许重复的成员在。set的基础上加入一个scores权重参数,使得集合中的参数可以按照权重进行排序。
三种特殊类型
- geospatial(GEO)类型:Redis 3.2 中增加了对GEO地理信息类型的支持。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
- HyperLogLog类型:Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的,是他也存在百分之0.81的错误率。同时 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。但是他也存在百分之0.81的错误率。
- bitmap 位图类型:bitmap就是通过最小单位bit来设置来进行0或者1的设置,来表示某个元素的值或者状态。一个bit的值只有0或1两种情况。因而bitmap比较适合用于统计状态。
redis常用的key和命令
Redis作为一个key-value数据库,对Key进行操作是无法避免的,通过进行对Redis-key的操作,来完成对数据库中数据的操作。
通用命令
命令 | 命令描述 | 命令示例 |
---|---|---|
keys pattern | 获取所有匹配pattern参数的keys。需要说明的是,在我们的正常操作中应该尽量避免对该命令的调用,因为对于大型数据库而言,该命令是非常耗时的,对redis服务器的性能打击也是比较大的。pattern支持glob-style的通配符格式,如*表示任意一个或多个字符,?表示任意字符,[abc]表示方括号中任意一个字母 | keys * |
del key [key …] | 从数据库删除中参数中指定的keys,如果指定键不存在,则直接忽略 | del key1 |
exists key | 判断指定键是否存在 | exists key1 |
rename key newkey | 为指定指定的键重新命名,如果参数中的两个keys的命令相同,或者是源key不存在,该命令都会返回相关的错误信息。如果newkey已经存在,则直接覆盖。 | rename key1 key2 |
renamenx key newkey | 当newkey不存在时,重命名key为newkey。其它条件和rename一致 | renamenx key1 key2 |
persist key | 如果key存在过期时间,该命令会将其过期时间消除,使该key不再有超时,而是可以持久化存储 | persist key1 |
expire key seconds | 该命令为参数中指定的key设定超时的秒数,在超过该时间后,key被自动的删除。如果该key在超时之前被修改,与该键关联的超时将被移除 | expire key1 50 |
ttl key | 获取该键的有效时长 | ttl key1 |
randomkey | 从当前打开的数据库中随机的返回一个key。 | randomkey |
type key | 获取与参数中指定键关联值的类型,该命令将以字符串的格式返回 | type key1 |
String类型
命令 | 命令描述 | 命令示例 |
---|---|---|
set key value | 设置有一个key的value值 | set user zhangsan |
setnx key value | 仅当key不存在时进行set,具备互斥性,存在时操作失败 | setnx user zhangsan |
setex key seconds value | set 键值对并设置过期时间 | setex lock 20 lock1 |
mset key value [key value …] | 设置多个key value | mset key1 value1 key2 value2 |
msetnx key1 value1 [key2 value2…] | 批量设置键值对,仅当参数中所有的key都不存在时执行,原子性操作,一起成功,一起失败 | msetnx key1 value1 key2 value2 |
get key | 返回key的value | get user |
mget key [key …] | 批量获取多个key保存的值 | mget key1 key2 |
exists key [key …] | 查询一个key是否存在 | exists user |
incr/decr key | 将指定key的value数值进行+1/-1(仅对于数字),非数字返回操作失败 | incr key1 |
incrby/decrby key n | 按指定的步长对数值进行加减 | incrby ke1 2 |
incrbyfloat key n | 为数值加上浮点型数值 | incrbyfloat key1 2.5 |
append key value | 向指定的key的value后追加字符串 | append user qw |
strlen key | 返回key的string类型value的长度 | strlen user |
getset key value | 设置一个key的value,并获取设置前的值,如果不存在则返回null | getseet user lisi |
应用场景:
- 热点数据缓存:将数据存储在redis中,利用redis支持高并发的特短板,可以大大加快系统的读写速度、降低后端数据库的压力,如码表、行政区划等常用热点数据缓存。
- Session共享:分布式系统环境下,客户端每次请求通过负载后都有可能命中不同的应用服务器,新命中的服务器无用户信息,导致要求客户端重新登录。将session信息存入redis后,当命中的服务器无该请求的session时,根据客户端携带的sessionID即可从redis中获取该session,正常执行请求。
- 验证码:客户端常有验证码操作,如登录、下载和重要操作前验证等。将userid、手机号等作为key,验证码作为value存储到redis中,设置过期时间,客户端输入验证码后从redis中取值对比,如果过期则取不到该数据,验证码过期无效。
- 计数器:利用String(int)自增操作作为计数器使用。用于访问量统计、互联网应用和app等中常出现的评论数、收藏数等数值计数。
- 存储对象:将对象序列化后的jsonString存入value,使用时congredis中获取值并反序列化为对象。不推荐使用,因为序列化和反序列化都会增大系统资源开销。
- 分布式锁(setnx):
1.持锁的服务出现异常导致key无法被删除,那么就会出现死锁,通过expire设置key有效时间,过期自动删除key,能够有效解决死锁问题
2.设置可以有效时间在极端情况下(执行expire操作时宕机等)仍可能会产生死锁,在web应用中可通过redisTemplate.opsForValue().setIfAbsent或lua脚本保证set 和expire的原子性来解决。
3.锁失控失效问题:在一般情况下上面的方式表面看是没有问题的,但是在高并发的情况下可能会存在问题。假设有这样的一个场景,我们redis锁设置10s的过期时间,当一个线程进来获取到锁之后,业务的执行时间达到了15s,此时线程1获得的锁已经是过期的锁,就是锁失效了。就表示此时如果有一个线程2进来可以获得锁。当线程2获得锁时候,此时,线程1 正好执行完毕,释放掉锁,就会导致线程1释放了线程2 的锁,会导致锁失控。使用待标识行的值作为value,释放锁时判断该锁是否为自己的锁,如果是才能执行释放。
4.redLock
hash类型
命令 | 命令描述 | 命令示例 |
---|---|---|
hset key field value | 将哈希表 key 中的字段 field 的值设为 value。重复设置同一个field会覆盖,返回0 | hset 100:car 102:good 5 |
hmset key field1 value1 [field2 value2…] | 同时将多个 field-value (域-值)对设置到哈希表 key 中 | hset 100:car 102:good 5 103:good 1 |
hsetnx key field value | 只有在字段 field不存在时,设置哈希表字段的值 | hsetnx 100:car 102:good 5 |
hget key field | 获取存储在哈希表中指定字段的值 | hget 100:car 102:good |
hmget key field1 [field2…] | 获取所有给定字段的值 | hmget 100:car 102:good 103:good |
hexists key field | 查看哈希表 key 中,指定的字段是否存在 | hexists 100:car 102:good |
hdel key field1 [field2…] | 删除哈希表key中一个/多个field字段 | hdel 100:car 102:good 103:good |
hincrby key field n | 为哈希表 key 中的指定字段的整数值加上增量n,并返回增量后结果 一样只适用于整数型字段 | hincrby 100:car 102:good 2 |
hincrbyfloat key field n | 为哈希表 key 中的指定字段的浮点数值加上增量 n | hincrbyfloat 100:car 102:good 2.5 |
hlen key | 获取哈希表中字段的数量 | hlen 100:car |
hgetall key | 获取在哈希表key 的所有字段和值 | hgetall 100:car |
应用场景:
购物车:以客户id作为key,每位客户创建一个hash存储结构存储对应的购物车信息
将商品编号作为field,购买数量作为value进行存储
配置参数
list类型
命令 | 命令描述 | 命令示例 |
---|---|---|
lpush/rpush key value1[value2…] | 从左边/右边向列表中push值(一个或者多个) | lpush list1 user1 |
lpushx/rpushx key value | 向已存在的列名中push值(一个或者多个),list不存在 lpushx失败 | lpushx list1 user1 |
linsert key before | after pivot value | 在指定列表元素的前/后 插入value |
lindex key index | 通过索引获取列表元素 | lindex list1 2 |
lrange key start end | 获取list 起止元素 (索引从左往右 递增 | lrange list1 0 -1 |
llen key | 查看列表长度 | llen list1 |
lpop/rpop key | 从最左边/最右边移除值 并返回 | lpop list1 |
lrem key count value | count >0:从头部开始搜索 然后删除指定的value 至多删除count个 count < 0:从尾部开始搜索… count = 0:删除列表中所有的指定value。 | lrem list1 1 user1 |
ltrim key start end | 通过下标截取指定范围内的列表 | ltrim list1 0 2 |
rpoplpush source destination | 将列表的尾部(右)最后一个值弹出,并返回,然后加到另一个列表的头部 | rpoplpush list1 list2 |
lset key index value | 通过索引为元素设值 | lset list1 2 user3 |
blpop/brpop key1[key2] timout | 移出并获取列表的第一个/最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 | blpop list1 2000 |
brpoplpush source destination timeout | 和rpoplpush功能相同,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 | brpoplpush list1 list2 2000 |
应用场景
消息队列(不建议)
最新列表list类型的lpush命令和lrange命令能实现最新列表的功能
set类型
命令 | 命令描述 | 命令示例 |
---|---|---|
sadd key member1[member2…] | 向集合中无序增加一个/多个成员 | sadd set1 s1 s2 s3 |
srem key member1[member2…] | 移除集合中一个/多个成员 | srem set1 s1 s2 |
scard key | 获取集合的成员数 | scard set1 |
smembers key | 返回集合中所有的成员 | smembers set1 |
sismember key member | 查询member元素是否是集合的成员,若存在返回1,不存在返回0 | sismember set1 s1 |
srandmember key [count] | 随机返回集合中count个成员,count缺省值为1 | srandmember set1 |
spop key [count] | 随机移除并返回集合中count个成员,count缺省值为1 | spop set1 |
sinter key1 [key2…] | 返回所有集合的交集 | sinter set1 set2 |
sinterstore destination key1[key2…] | 在sinter的基础上,存储结果到集合中。覆盖 | sinterstore set3 set1 set2 |
sunion key1 [key2…] | 返回所有集合的并集 | sunion set1 set2 |
sunionstore destination key1 [key2…] | 在sunion的基础上,存储结果到及和张。覆盖 | sunionstore set3 set1 set2 |
sdiff key1[key2…] | 返回所有集合的差集 key1- key2 - … | sdiff set1 set2 |
sdiffstore destination key1[key2…] | 在sdiff的基础上,将结果保存到集合中。覆盖 | sdiffstore set3 set1 set2 |
smove source destination member | 将source集合的成员member移动到destination集合 | smove set1 set2 s1 |
应用场景:
- 好友/关注/粉丝/感兴趣的人集合:set类型唯一的特点使得其适合用于存储好友/关注/粉丝/感兴趣的人集合,集合中的元素数量可能很多,每次全部取出来成本不小,set类型提供了一些很实用的命令用于直接操作这些集合:
1)sinter命令可以获得A和B两个用户的共同好友
2)sismember命令可以判断A是否是B的好友
3)scard命令可以获取好友数量
4)关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合 - 黑名单/白名单:经常有业务出于安全性方面的考虑,需要设置用户黑名单、ip黑名单、设备黑名单等,set类型适合存储这些黑名单数据,sismember命令可用于判断用户、ip、设备是否处于黑名单之中
zset
命令 | 命令描述 | 命令示例 |
---|---|---|
zadd key score member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 | zadd zset1 1 val1 |
zcard key | 获取有序集合的成员数 | zcard zset1 |
zscore key member | 返回有序集中,成员的分数值 | zscore zset1 val1 |
zcount key min max | 计算在有序集合中指定区间score的成员数 | zcount zset1 1 100 |
zincrby key n member | 有序集合中对指定成员的分数加上增量 n | zincrby zset1 2 val1 |
zrank key member | 返回有序集合中指定成员的索引 | zrank zset1 1 val1 |
zrange key start end | 通过索引区间返回有序集合成指定区间内的成员 | zrange zset1 0 10 |
zrangebyscore key min max | 返回有序集中指定分数区间内的成员 -inf 和 +inf分别表示最小最大值,只支持开区间 | zrangebyscore zset1 1 100 |
zrevrangebyscore key max min | 返回有序集中指定分数区间内的成员,分数从高到低排序 | zrevrangebyscore zset1 1 100 |
zrem key member1 [member2…] | 移除有序集合中一个/多个成员 | zrem zset1 val1 |
zremrangebyscore key min max | 移除有序集合中给定的分数区间的所有成员 | zremrangebyscore zset1 1 100 |
应用场景:
排行榜:实现各类排行榜。以排行榜标识作为 zset 的 key,把排序对象作为 member ,对应评分等作为 score,当 score 发生变化时更新 score。利用 ZREVRANGE 或者 ZRANGE 查到对应数量的记录
滑动窗口限流
hyperloglog
基础数据类型为string,这种数据类型一般是为了计算集合中的基数,应用:注册人数,
pfadd 添加元素
pfcount 统计基数
pfmerge 合并两个hyperloglog ,集群模式下,需要具有相同slot
bitmaps
适用于只有两种状态的数据存储,应用:每日上线人数,打卡,每日是否登录
setbit 设置位图数据
getbit 获取位图数据
bitcount 统计为1的个数
bitop 位操作 一般有or xor and
geospatial
存储地理坐标,应用:附近的人,位置共享,距离计算
geoadd 添加地理位置
geodist 获取集合中两个地理位置的距离
geopos 返回成员的经度纬度
georadius 获取半径内的所有地理位置
georadiusbymember 根据成员元素来搜索范围内的所有地理位置
四、Redis持久化
redis的数据都是存储于内存中的,那么假如一旦发生了宕机,数据都将丢失,那么为了解决这个问题,redis提供了相应的持久化机制rdb(redis database)与aof(appendonly file)
客户端发送命令到服务端,服务端接受命令并执行,执行完成后开始持久化,服务端调用write将数据写到内存缓冲区,操作系统将内存缓冲区的数据移交到磁盘控制器,也就是磁盘缓存,最后磁盘控制器真正写入数据到磁盘上
RDB
RDB是redis默认的持久化方式,当开启save属性时,redis会把内存中的当前数据集快照写入磁盘,也就是snapshot。
触发方式
手动触发:
- 调用save命令,redis阻塞当前线程,此时redis停止处理其他命令,直到整个rdb过程完成,对于需要redis及时响应的系统,此种方式不推荐
- bgsave命令,redis会fork一个新的进程来完成RDB,完成后自动结束此时redis可以处理其他的命令,阻塞过程只发生在fork新线程时。
- 执行shutdown命令,redis会保存当前内存快照(一般不会使用,假如此时redis丢失了大部分的数据,已存盘的RDB会被新生成的RDB文件覆盖,造成严重后果)
- 执行flushall命令,redis会保存一个空的RDB文件
自动触发:
- 在配置文件中配置redis的持久化条件,如下
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存
RDB优缺点
优点:
- RDB文件是一个紧凑压缩的二进制文件,代表redis某个时间点上的内存快照,非常适合做全量复制,容灾备份的场景,适合大规模的数据恢复
- Redis加载RDB的速率远远高于AOF
- RDB在完成时会fork新的进程来完成存盘,主线程可以继续提供读写服务.
缺点:
- RDB没办法做到实时数据同步,在最后一次存盘后假如redis宕机了,RDB快照数据可能会丢失大量数据
- RDB每次bgsave会fork新的子进程来完成快照,频繁执行会带来不少的开销,假如不开启rdb压缩,那么内存膨胀量为工作进程的两倍,存盘完成后,替换上一次存盘的.rdb文件
解释:
fork的作用是复制一个与当前进程一样的进程 。 新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程 。
AOF
- 当开启AOF持久化后,所有的写命令会传输到AOF_BUF缓存区中
- 当任一命令执行,调用aof.c/flushAppendOnlyfile函数,里面调用了write、save函数
- write:将缓冲区写入aof文件;save:将文件写入磁盘,调用fsync或fdatasync
触发策略
appendfsync always #每次有数据修改发生时都会写入AOF文件,性能差,安全性最高,最多丢失1个命令。
appendfsync everysec #每秒钟同步一次,该策略为AOF的默认策略。如果一秒内宕机,1s数据丢失
appendfsync no #从不同步。高效但是数据不会写入磁盘,但save命令执行后,数据也会写入磁盘,宕机后数据基本丢失。
- appendfsync no
此种模式下,当调用flushappendonlyfile函数时,只会执行write函数(阻塞主线程),save函数只会在1.redis关闭,2.aof功能关闭时,3系统的写缓冲刷新(一般为缓冲区满了)执行,此时redis会阻塞主进程 - appendfsync everysec (async)
每一秒执行一次调用 flushAppendOnlyFile 函数,执行write函数(阻塞主线程),此种模式是fork子进程去调用save,不会引起主进程阻塞,但每不是每秒都会里面的write和save方法 - appendfsync always (sync)
只要有命令执行,会调用flushappendonlyfile,同时执行write和save函数,主进程执行save命令,此时主进程阻塞 ,不推荐使用
AOF重写
随着服务器运行时间的流逝,AOF文件中的内容越来越多,文件体积越来越大,如果不加以控制,会对redis服务器甚至宿主计算器造成影响
当aof达到一定大小,会触发aof重写,也就是执行BGREWRITEAOF命令,把相似命令组装,减少aof占用空间,如:
set k1 v1
set k2 v2
set k3 v3
重写后:
set k1 v1 k2 v2 k3 v3
减少磁盘占用
AOF重写特性:
- 重写过程耗时较长,redis会fork一个子进程来完成AOF重写,服务器继续处理其他请求,同时设立AOF重写缓冲区,新写入的命令会进入AOF重写缓冲区
- 当子进程完成重写后,向主进程发送信号,主进程阻塞,将aof重写缓冲区的的内容写入到新的aof文件中,覆盖旧的aof文件,完成后继续处理服务器其他命令
缺点
- 相同数据集情况下,aof文件体积大于rdb
- aof的效率低于rdb
同时开启两种持久化时,优先加载AOF
五、Redis主从复制
注意:假如1个master连接多台从节点,所有从节点同时宕机,而此时所有从节点重启后开始全量复制,master节点可能会因为io瓶颈宕机
全量复制
- slave连接master,发送sync命令,
- master收到命令后,bgsave生成rdb,并利用缓冲区收集所有此后待执行命令,
- master生成rdb后,向slave发送rdb,并继续收集执行命令,等到rdb发送完成后,向slave发送缓冲区命令
- slave收到快照,丢弃所有旧数据,载入快照,待载入完成后并继续接收来自master的缓冲区命令,
- 复制完成后,双方维护长连接并彼此发送心跳。
增量复制
- slave已完成全量复制并开始正常工作,master每执行一个命令便会向所有slave节点发送命令,从服务器接收命令并同步。
六、Redis集群
主从模式
架构一般为1主1从、1主多从,相比于单机版,为master节点增加了多台从服务器, 并且假如master节点挂掉,有从节点继续提供写服务,而从节点的加入可以提高并发量,master提供读服务,从节点提供写服务作用:数据冗余、负载均衡、高可用、故障恢复
优点
- 数据冗余:主从复制完成,从节点具有相同数据副本
- 可用性:master挂掉,从节点继续提供读服务
- 负载:主从复制的过程是非阻塞的,主从继续提供读写服务
- 性能提高,master节点提供写服务,slave节点提供读服务,并发性能提高,做到读写分离
缺点
- 不具备容错和恢复机制,master宕机,主从不再提供写服务,只有从节点提供读服务,继续使用写服务只能重启master,而前端读服务也必须手动切换ip
- master宕机如果发生在增量复制过程种,可能主节点和从节点的数据不一致,降低可用性
- 假如发生网络抖动,master与slave又会开启一次全量复制,对io是不小的消耗。
- 不支持横向扩展,难以扩容
哨兵模式
哨兵模式在主从的基础上,增加了哨兵监控主从集群状态,哨兵本身不提供读写能力,原理是,
- 哨兵发送向所有redis节点(包括主、从)发送命令,等待redis响应,redis收到后,返回运行状态
- 假如master节点发生宕机,哨兵无法收到master节点的监控信息,会主动将1台从节点切换为master节点,并且以发布订阅模式通知所有从节点,修改配置信息。
故障切换(failover)
上面说到假如哨兵监控到master下线,会将slave切换到master节点的具体过程:
- 假如一台哨兵发现master节点宕机,此时不会马上做切换,只是哨兵1标记master为主观下线
- 当后面的哨兵也发现master节点主观下线,那么哨兵之间会发起一次投票,投票结果由一个哨兵发起,开始failover操作
- failover完成后,各个哨兵会以发布订阅模式通知自己下属的从节点切换连接的主节点,修改配置文件,也就是客观下线
sentinel配置
- sentinel monitor mymaster ip port 2 //配置需要监控的主节点,这里2代表故障切换需要多少个哨兵标记master主管下线,当哨兵标记主观下线的数量达到这个数量,开启故障切换
- sentinel down-after-milliseconds 30000 //哨兵认定master主观下线的时间,单位毫秒,默认为30s
优点
- 具有主从模式的所有优点
- 故障切换:自动切换故障主节点,具有更高的可用性
缺点
- 无法扩容,所有主从节点保存相同副本,增加过多节点反而会影响集群性能
- 配置繁琐
集群模式
哨兵模式基本保障高可用、读写分离,但所有节点都保存相同的数据,很浪费内存的同时具有存储瓶颈,集群模式就是在此基础上做了优化,采用无中心化结构,实现了分布式存储,所有redis节点彼此互联,内部采用二进制协议传输带宽,节点的fail只有超过半数的节点检测才有效,客户端直连redis节点,并且只需要连接任意节点都可使用。
Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。
注意:redis集群中,从节点一般是不参与读写的,只是被当成备用节点,而主节点同时提供读写服务,这一点和主从模式、哨兵不同,从节点只是用于故障转移。
Redis集群至少需要3个主节点,为什么不用4个节点
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。
例如:
在9个master的架构中,如果4台master故障,通过过半机制,redis可以选举新的master。如果5台master故障无法选举新的master
在10个master的架构中,如果4台master故障,通过过半机制,redis可以选举新的master。如果5台master故障无法选举新的master
在高可用方面,9台master与10台master一致。所以通常会使用奇数。假设现在reids内存不足需要拓展,我们将master的数量加到11台,就高可用方面来说,就算其中5台master发送故障,也可以自动选举新的master。
优点
- 无中心架构,支持高可扩,理论上支持无线扩展,可支持更高的并发量
- 数据按slot分散到多个节点,可动态调整数据分布
- 高可用:部分节点挂掉,相应的slave会升级成master节点继续提供服务,实现故障切换,投票选举master节点
缺点
- 假如集群中一个节点(包括master和所有连接的备用节点)挂掉,整个集群不可用,配置cluster-require-full-coverageyes no 其他小集群可以继续提供服务,但不建议这么做,
数据存储方式
Redis的存储采用数据分片的方式,也就是hash slot来存储数据,一个redis cluster 最多有16384个hash slot,存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个。按照槽来进行分片,通过为每个节点指派不同数量的槽,可以控制不同节点负责的数据量和请求数.
哈希槽计算公式
集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和。
而我们常用的java客户端如jedis 也是按照这个规则来获取相应节点的连接的
Jedis源码解析
//JedisConnectionFactory
new JedisConnectionFactory(redisClusterConfiguration);
//JedisClusterConnectionHandler
public JedisClusterConnectionHandler(Set<HostAndPort> nodes, GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password) {
this.cache = new JedisClusterInfoCache(poolConfig, connectionTimeout, soTimeout, password);
this.initializeSlotsCache(nodes, poolConfig, password);
}
//JedisClusterInfoCache
public void discoverClusterNodesAndSlots(Jedis jedis) {
this.w.lock();
try {
this.reset();
//发现集群节点hash分配情况,调用cluster slots
List<Object> slots = jedis.clusterSlots();
Iterator var3 = slots.iterator();
//遍历集群几点
while(true) {
List slotInfo;
do {
if (!var3.hasNext()) {
return;
}
Object slotInfoObj = var3.next();
slotInfo = (List)slotInfoObj;
} while(slotInfo.size() <= 2);
//获取当前节点hash数据
List<Integer> slotNums = this.getAssignedSlotArray(slotInfo);
int size = slotInfo.size();
for(int i = 2; i < size; ++i) {
List<Object> hostInfos = (List)slotInfo.get(i);
if (hostInfos.size() > 0) {
HostAndPort targetNode = this.generateHostAndPort(hostInfos);
this.setupNodeIfNotExist(targetNode);
if (i == 2) {
//缓存slots信息
this.assignSlotsToNode(slotNums, targetNode);
}
}
}
}
} finally {
this.w.unlock();
}
}
//DefaultValueOperations
public V get(Object key) {
return this.execute(new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);
}
//JedisClusterConnection
public byte[] get(byte[] key) {
try {
return this.cluster.get(key);
} catch (Exception var3) {
throw this.convertJedisAccessException(var3);
}
}
//BinaryJedisCluster
public byte[] get(final byte[] key) {
return (byte[])(new JedisClusterCommand<byte[]>(this.connectionHandler, this.maxAttempts) {
public byte[] execute(Jedis connection) {
return connection.get(key);
}
}).runBinary(key);
}
//JedisClusterCommand
public T runBinary(byte[] key) {
if (key == null) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
} else {
return this.runWithRetries(key, this.maxAttempts, false, false);
}
}
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
if (attempts <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
} else {
Jedis connection = null;
Object var7;
try {
if (asking) {
connection = (Jedis)this.askConnection.get();
connection.asking();
asking = false;
} else if (tryRandomNode) {
connection = this.connectionHandler.getConnection();
} else {
connection = this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
Object var6 = this.execute(connection);
return var6;
} catch (JedisNoReachableClusterNodeException var13) {
throw var13;
} catch (JedisConnectionException var14) {
this.releaseConnection(connection);
connection = null;
if (attempts <= 1) {
this.connectionHandler.renewSlotCache();
throw var14;
}
var7 = this.runWithRetries(key, attempts - 1, tryRandomNode, asking);
return var7;
} catch (JedisRedirectionException var15) {
if (var15 instanceof JedisMovedDataException) {
this.connectionHandler.renewSlotCache(connection);
}
this.releaseConnection(connection);
connection = null;
if (var15 instanceof JedisAskDataException) {
asking = true;
this.askConnection.set(this.connectionHandler.getConnectionFromNode(var15.getTargetNode()));
} else if (!(var15 instanceof JedisMovedDataException)) {
throw new JedisClusterException(var15);
}
var7 = this.runWithRetries(key, attempts - 1, false, asking);
} finally {
this.releaseConnection(connection);
}
return var7;
}
}
//JedisSlotBasedConnectionHandler
public Jedis getConnectionFromSlot(int slot) {
JedisPool connectionPool = this.cache.getSlotPool(slot);
if (connectionPool != null) {
return connectionPool.getResource();
} else {
this.renewSlotCache();
connectionPool = this.cache.getSlotPool(slot);
return connectionPool != null ? connectionPool.getResource() : this.getConnection();
}
}
move重定向
1、每个节点通过通信都会共享Redis Cluster中槽和集群中对应节点的关系;
2、客户端向Redis Cluster的任意节点发送命令,接收命令的节点会根据CRC16规则进行hash运算与16383取余,计算自己的槽和对应节点;
3、如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端;
4、如果保存数据的槽不在当前节点的管理范围内,则向客户端返回moved重定向异常;
5、客户端接收到节点返回的结果,如果是moved异常,则从moved异常中获取目标节点的信息;
6、客户端向目标节点发送命令,获取命令执行结果;
# redis-cli -c -p 6376
127.0.0.1:6376> ping
PONG
127.0.0.1:6376> set k1 v1
-> Redirected to slot [12706] located at 172.20.21.162:6375
OK
172.20.21.162:6375> set k1 v2
OK
172.20.21.162:6375> set k3 v3
-> Redirected to slot [4576] located at 172.20.21.162:6371
OK
172.20.21.162:6371>
七、Redis发布订阅模型
这种消息模式主要是由3种角色构成,publisher,channel,subscriber,订阅者订阅相应的频道,等待接受消息,而发布者发布消息到对应的频道上,对应频道上的所有订阅者,都会收到发布者发布的消息。
subscrible 发布订阅命令
unsubscrible 取消订阅
实时广播,实时提醒,订阅,关注系统可以采用发布订阅模式
适用于简单的场景
八、Redis扩展
与Spring融合
@Bean
public GenericObjectPoolConfig<?> genericObjectPoolConfig() {
GenericObjectPoolConfig genericObjectPoolConfig=new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxIdle(20);
genericObjectPoolConfig.setMaxTotal(100);
genericObjectPoolConfig.setMinIdle(10);
genericObjectPoolConfig.setMaxWaitMillis(3000L);
return genericObjectPoolConfig;
}
@Bean
public LettuceClientConfiguration lettuceClientConfiguration(GenericObjectPoolConfig genericObjectPoolConfig) {
return LettucePoolingClientConfiguration.builder()
.poolConfig(genericObjectPoolConfig)
.build();
}
@Bean
public RedisClusterConfiguration redisClusterConfiguration(){
List<RedisNode> redisNodeList= Arrays.asList(environment.getProperty("spring.redis.cluster.nodes").split(",")).stream().map(str->new RedisNode(str.split(":")[0],Integer.parseInt(str.split(":")[1]))).collect(Collectors.toList());
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
redisClusterConfiguration.setClusterNodes(redisNodeList);
if(!password.equals("-1")){
redisClusterConfiguration.setPassword(password);
}
redisClusterConfiguration.setMaxRedirects(Integer.parseInt(environment.getProperty("spring.redis.cluster.max-redirects")));
return redisClusterConfiguration;
}
@Bean
public RedisConnectionFactory lettuceConnectionFactory(RedisClusterConfiguration redisClusterConfiguration,LettuceClientConfiguration lettuceClientConfiguration) {
LettuceConnectionFactory lettuceConnectionFactory= new LettuceConnectionFactory(redisClusterConfiguration,lettuceClientConfiguration);
return lettuceConnectionFactory;
}
@Bean
public RedisTemplate<String,Serializable> redisTemplate(RedisConnectionFactory lettuceConnectionFactory){
RedisTemplate<String,Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
redisTemplate.setHashKeySerializer(StringRedisSerializer.UTF_8);
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean("redisCacheManager")
public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory) {
return RedisCacheManager.builder(lettuceConnectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith((cacheName) -> prefix+cacheName)
.entryTtl(Duration.ofSeconds(expire))).build();
}
SpringCache融合
- @Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。 - @CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上 - @CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库
如果需要对每个缓存列设置过期时间,请使用自定义注解做缓存
Redis运行速度变慢
如果当前redis存储需要占用的内存超过了redis可使用的最大内存,操作系统会将不可用的数据或老旧数据放至swap区(windows也存在这种功能分区,叫虚拟内存),从而给redis腾出更多的运行内存,但swap分区实际是用硬盘实现的,所以读写效率远远无法跟内存比较,这时候redis的性能会受到极大影响
- 硬件手段,加内存
- 如果缓存数据较小,使用32位的redis实例,32实例数据结构只占64位数据的一半
- 设置key的过期时间,避免数据永久保存
- 使用hash结构数据存储数据
- 主动设置key回收策略,当内存不够时,执行回收
RedisTemplate的序列化
如果使用的StringRedisTemplate 那么key\value序列化的方式默认为StringRedisSerializer.UTF-8
假如使用的是RedisTemplate 那么key\value采用的是JDKSerializationRedisSerializer
推荐 key和hashkey的序列化策略为StringRedisSerializer
hashvalue的序列化为GenericJackson2JsonRedisSerializer
科普:什么是序列化?
为了满足将一个已经实例化的对象通过io传递到其他机器,而产生的序列化与反序列化的概念。序列化:将内存中对象压缩成字节流,而反序列化就是将字节流转化为内存中的对象;
Java中,需要序列化的对象需要实现Serializable接口
分布式锁
RReadWriteLock rwlock = redisson.getLock("anyRWLock");
rwlock.readLock().lock();
rwlock.writeLock().lock();
// Lock time-to-live support
// releases lock automatically after 10 seconds
// if unlock method not invoked
rwlock.readLock().lock(10, TimeUnit.SECONDS);
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// Wait for 100 seconds and automatically unlock it after 10 seconds
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
....
lock.unlock();
缓存穿透(查不存在的数据)
首先,查询一个数据的流程为用户发送请求,后台服务器收到请求后,查询redis缓存,如果有数据直接返回,如果没有数据就去查询数据库再返回给用户; 但假如我们查询的数据的key,在redis中没有,那么这个用户的请求一定会去数据库查询,然后再返回,假设如果有恶意的攻击,那么数据库在一瞬间可能直接崩溃,后果不堪设想
解决方案:
- 布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层进行校验,不符合的就丢弃,减轻查询压力
- 缓存空对象,存在问题:1.如果控制被缓存,那么缓存中会有大量的空值;2.即使对空值设置了过期时间,还是会有缓存层和存储层的数据不一致的情况,这对需要保证一致性的业务,会有影响
- 在数据层对查询id拦截,和布隆过滤器想法相似,获取当前业务数据的最大最小id号码,假如数据的id不在区间中,那么就拦截,只能针对特殊的查询可以使用,不具有通用性
缓存击穿(缓存时间到期)
在一瞬间,某个热点数据失效,所有的请求不经过缓存直接打到数据库上,数据库瞬间接收到巨大压力,造成系统瘫痪
解决方案:
- 设置热点数据永不过期,但一直会占用redis内存
- 加互斥锁,使用分布式锁,其实也就保证对每一个key同时只有一个线程去查询后端服务,其他线程没有获得权限
RLock lock = redisson.getLock("anyLock");
try{
// 3. 尝试加锁,最多等待3秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
if(res){ //成功
// do your business
Object o=getDataFromDB();
putDataToRedis(o)
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
缓存雪崩(大批量缓存过期)
在某一个时间段,缓存集体失效、redis宕机 例如:双11,设置缓存的时间为12点,过期时间为1个小时,那么到了1点的时候,所有的缓存全部失效,那么所有的访问查询全部都会打到数据库上,可能瞬间数据库就会宕机
解决方案:
- 停掉一些不重要的服务,(保证主要的业务能正常执行)
- 限流降级:在大量缓存失效后,通过加锁或队列来控制读数据写缓存的数量,比如对同一个key,只允许一个线程去查询数据和写缓存,其他线程等待
- redis高可用:横向扩展redis,(异地多活)
- 数据预热:在正式部署之前,把所有可能访问的数据都预先访问一遍。这样可能访问的数据都会被加载到缓存中,在即将发生大并发访问前,手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀;
Options.set(finalRedis_result, 1000 * 60*5 - new Random().nextInt(1000 * 20), TimeUnit.MILLISECONDS);
lua脚本
一次性发送多个命令,减少网络开销。原子性,命令的复用,redis缓存lua脚本,减少网络开销
每个用户在Ⅹ秒内只能访问Y次
ip_limit.lua
IP限流,对某个IP频率进行限制,5分钟 访问10次
--获取KEY
local key = KEYS[1]
--获取ARGV内的参数
local expire = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
--获取key的次数
local current = redis.call('get', key)
--如果key的次数存在且大于预设值直接返回当前key的次数
if current and tonumber(current) >= count then
return 0;
end
--进行自增
current = redis.call('incr', key)
--获取key的过期时间
local ttl = redis.call('ttl', key)
--如果是第一次自增,设置过期时间
if tonumber(current) == 1 then
redis.call('expire', key, expire)
else
--如果key过期时间是永久,重新设置过期时间
if ttl and tonumber(ttl) == -1 then
redis.call('expire', key, expire)
end
end
--返回key的次数
return tonumber(current)
./src/redis-cli -p 9999 --eval ip_limit.lua 192.168.0.1 , 1 1
redis pipeline
未使用pipeline
使用了pipeline
jediscluster或lettuce集群 本身并不支持pipeline,如果对redis比较熟练,可以改写jediscluster底层逻辑,来实现集群下的pipeline,现成方案使用redisson客户端来完成pipeline
RedissonClient redisson = Redisson.create();
RBatch batch = redisson.createBatch(BatchOptions.defaults());
batch.getMap("test1").fastPutAsync("1", "2");
batch.getMap("test2").fastPutAsync("2", "3");
batch.getMap("test3").putAsync("2", "5");
RFuture<Long> future =batch.getAtomicLong("counter").incrementAndGetAsync();
batch.getAtomicLong("counter").incrementAndGetAsync();
future.whenComplete((res, exception) -> {
// ...
});
BatchResult<?> res = batch.execute();
Long counter = (Long) res.getResponses().get(3);
future.get().equals(counter);
redisson.shutdown();
一级、二级缓存融合方案
为了提高系统的性能, 可以使用本地缓存+redis+db来提供服务器的吞吐量,避免redis被打垮,但同时了也增加了系统的复杂性
如果数据库更新数据,
如何保证三者数据一致性
- redis与mysql的一致性
- 本地缓存与mysql的一致性
redis与db同步
- 延迟双删
2.更新数据库
3.延时500毫秒
4.删除redis
问题一:为何要延时500毫秒?
这是为了我们在第二次删除redis之前能完成数据库的更新操作。
假象一下,如果没有第三步操作时,有很大概率,在两次删除redis操作执行完毕之后,数据库的数据还没有更新,此时若有请求访问数据,便会出现我们一开始提到的那个问题。
问题二: 为何要两次删除redis?
如果我们没有第二次删除操作,此时有请求访问数据,有可能是访问的之前未做修改的redis数据,删除操作执行后,redis为空,有请求进来时,便会去访问数据库,此时数据库中的数据已是更新后的数据,保证了数据的一致性。
本地缓存数据同步方案
- MQ(推荐)