1. Redis
NoSQL:非关系数据库,以key-value模式存储,可以减少CPU和IO压力(比如分布式服务器将Session存在NoSQL中,通过内存读取),也可以直接作为缓存使用。
不遵循SQL标准,不支持ACID(可以支持事务),性能远超SQL,适用于海量、高并发、可扩展的数据读写
Redis:数据存在内存中,但是支持持久化;不仅支持Key-value模式,还支持list、set、hash等,一般作为缓存数据库辅助持久化数据库
Redis的默认端口号:6397
Redis默认 16 个数据库,类似数组下标从 0 开始,默认使用0号库,所有库密码相同
>select <id> 选择库
>dbsize 查看库中的key数量
Redis是单线程+多路IO复用技术
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NKPOIzID-1663590849426)(F:\1-找工作学习\typora图片\性能优化学习\单线程和多路IO复用.png)]
1. 常用数据类型
Redis中有五大常用数据类型:String,List,Set,Hash,Zset(有序集合)
> keys * 查看当前库所有 key
> exists key1 判断 key1 是否存在
> type key1 查看 key1 是什么类型
> del key1 删除指定的 keys1 数据
> unlink key1 异步删除
> expire key1 10 为key1设置10秒过期时间
> ttl key1 查看还有多少秒过期,-1 表示永不过期,-2 表示已过期
1) String 类型
String类型是Redis的基本类型,String是二进制安全的,只要能转换成字符串,就可以用String保存,比如jpg图片、序列化对象。最大长度512M
二进制安全:是一种主要用于字符串操作函数的计算机编程术语。只关心二进制化的字符串,不关心具体的字符串格式,严格的按照二进制的数据存取。这保证字符串不会因为某些操作而遭到损坏。
- 常用命令:
> set <key><value> 添加键值对,相同键会覆盖
> get <key>
> append <key><value> 将给的<value>加到<key>原值的末尾
> strlen <key> 获得值的长度
> setnx <key><value> 只有在 key 不存在时 设置 key 的值
> incr <key> 将 key 中储存的数字值增 1,只能对数字值操作,如果为空,新增值为 1
> incrby <key><步长> 自定义加多少
> decr <key> 将 key 中储存的数字值减 1,只能对数字值操作,如果为空,新增值为-1
> decrby <key><步长> 自定义减多少
> mset/mget/msetnx <key1><value1><key2><value2> ... 同时操作多个
> getrange <key><起始位置><结束位置> 获得范围内的值,包括开始和结束
> setrange <key><起始位置><value> 用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始
> setex <key><过期时间><value> 设置键值的同时设置过期时间
incr/decr 是原子操作,不会被线程调度机制打断(因为Redis是单线程的)
- 底层结构
String的数据结构是简单动态字符串,类似于java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
2) 列表(List) 类型
底层是双向链表,有序。
- 常用命令
> lpush/rpush <key><value1><value2><value3>... 从左/右边插入一个或多个值(创建)
> lpop/rpop <key>从左边/右边吐出一个值。值在键在,值光键亡
> rpoplpush <key1><key2>从<key1>列表右边吐出一个值,插到<key2>列表左边
> lrange <key><start><stop> 按照索引范围获得元素(从左到右)0 -1表示所有元素
> lindex <key><index> 按照索引下标获得元素(从左到右)
> llen <key> 获得列表长度
> linsert <key> before<value><newvalue> 在<value>的后面插入<newvalue>插入值
> lrem <key><n><value> 从左边删除 n 个 value(从左到右)
> lset<key><index><value> 将列表 key 下标为 index 的值替换成 value
- 数据结构
List的数据结构为快速链表quikList。它在元素较少时会使用一块连续的内存存储ziplist(压缩列表),当数据量比较多的时候变成多个zipList通过链表链接的结构
3) 集合(Set) 类型
无序,没有重复元素。底层是一个hash表,添加删除查找的复杂度都是 O ( 1 ) O(1) O(1)
- 常用命令
> sadd <key><value1><value2> .... 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
> smembers <key> 取出该集合的所有值
> sismember <key><value> 判断集合<key>是否为含有该<value>值,有 1,没有 0
> scard<key> 返回该集合的元素个数
> srem <key><value1><value2>...删除集合中的某个元素
> sinter <key1><key2> 返回两个集合的交集元素。
>……
- 数据结构
Set数据结构是dict字典,字典是用哈希表实现的
4) 哈希(Hash) 类型 hash类似于Java中的Map
包括key 和value vaue里包含field和value的映射表
- 常用命令
> hset <key><field><value> 给<key>集合中的 <field>键赋值<value>
> hmset <key1><field1><value1><field2><value2>... 批量设置 hash 的值
> hexists<key1><field> 查看哈希表 key 中,给定域 field 是否存在
> hkeys <key> 列出该 hash 集合的所有 field
> hvals <key> 列出该 hash 集合的所有 value
> hincrby <key><field><increment>为哈希表 key 中的域 field 的值加上增量 1 -1
- 数据结构
两种:当长度较短,数量较少时,使用ziplist 否则用hashtable
5) 有序集合(Zset) 类型
每个成员关联一个评分,根据评分进行排序
- 数据结构
底层数据结构包括两个:
1)hash 哈希中的value就包括value和score保障元素 value 的唯一性,可以通过元素 value 找到相应的 score 值。
2)跳跃表,目的是根据score查找更高效
6) 新数据类型-Bitmaps
value中是专门进行位操作的字符串
- 常用命令
> setbit<key><offset><value> 设置 Bitmaps 中某个偏移量的值(0 或 1),按位操作
> getbit<key><offset> 获取 Bitmaps 中某个偏移量的值,按位
> bitcount<key>[start end] 统计字符串中1的个数 start和end是字节索引,8位
> bitop and(or/not/xor) <destkey> [key…] 对一或多个key运算,并保存到destkey
- Bitmaps 与 set 对比
分别表示存储活跃用户,活跃用户占比越多,使用bitmaps越能节省存储空间
7) 新数据类型-HyperLogLog
专门用来做基数统计,所需空间非常小。HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素
- 常用命令
> pfadd key element [element ...] 添加指定元素到 HyperLogLog 中。
> pfcount key [key ...] 返回给定 HyperLogLog 的基数估算值。
> pfmerge destkey sourcekey [sourcekey ...] 将多个合并为一个 HyperLogLog
2. Jedis
java中操作数据库redis的工具,maven中导入相关依赖即可使用jedis
命令和控制台命令类似
3. 整合SpringBoot
- 新建SpringBoot项目
- 在 pom.xml 文件中引入 redis 相关依赖
- application.properties 配置 redis 配置 (链接数据库以及相关配置)
- 添加Redis配置类。新建一个类(内容固定)
- 新增一个Controller,就可以通过浏览器访问测试
4. 事务和锁机制
Redis事务是一个单独的隔离操作,事务中的命令顺序执行,不会被别的命令插队
- Multi、Exec、Discard
输入multi
,开启队列,之后输入的命令一次进入命令队列中(组队过程)
输入discard
,可以在组队过程中放弃组队
输入exec
,开始顺序执行队列中的命令
- 错误处理
组队中某个命令出错,整个队列都会被取消,无法执行(编译错误)
执行中某个命令出错,只有该条命令不执行,其他命令正常执行(运行错误)
- 悲观锁
在拿数据时,做操作之前就上锁,传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁**,**写锁等
- 乐观锁
数据有版本号,拿数据时不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新
这个数据(版本号是否一致),更新时修改版本号。
适用于多读的应用类型,Redis 就是利用这种 check-and-set 机制实现事务的。
- WATCH key [key …] 监视一个或多个key
乐观锁机制。在执行multi之前添加,如果用户A添加key1的监视,将key1的操作加入到multi中,这时用户B修改了key1,用户A执行exec时,事务执行中断。
因为用户B修改key1时,修改了版本号,用户A执行key1相关操作时,会检查到版本号不一致
-
Redis事务的三特性
- 单独的隔离操作:事务中的命令顺序执行,不会被其他命令请求打断
- 没有隔离级别的概念:事务提交前任何指令都不会被实际执行
- 不保证原子性:会出现一条命令执行失败,其他命令正常的情况,不会回滚
-
秒杀模拟
模拟操作:
1.判断用户uid和商品pid非空
2.链接jedis
Jedis jedis=new Jedis("192.168.**.**",6379);
3. 拼接key,得到存储在Redis中的key
4. 获取pid库存,如果库存为null,表示秒杀还没开始
5. 如果库存为0表示秒杀结束
6. 判断用户是否存在(存在set中)
if(jedis.sismember(userKey,uid)){
System.out.println("用户已存在");
jedis.close();
return false;
}
7. 秒杀:商品pid数减一,用户清单加一
jedis.decr(proKey);
jedis.add(userKey,uid);
System.out.println("秒杀成功");
jedis.close();
return true;
该方案存在的问题:
- 超卖现象(使用工具ab测试)
- 链接超时
改进:
1.链接超时问题使用链接池解决,连接池使用单例模式双重锁检查机制
public static JedisPool getJedisPoolInstance(){
if(jedisPool==null){
synchronized(JedisPoolUtil.class);
if(jedisPool==null){
……
jedisPool=new JedisPool(……);
}
}
return jedisPool;
}
代码通过jedisPoolInstance获得连接池对象
2.超卖问题使用乐观锁解决,对ProKey增加watch,添加事务
jedis.watch(proKey);
Transcation multi=jedis.multi();
multi.decr(proKey);
multi.sadd(userKey,uid);
List<Object> results=multi.exec();
if(result==null||result.size()){ 失败 }
新的问题:使用乐观锁会产生库存遗留问题,比如100个人同时检查库存,有一个人购买成功,修改版本号,则剩下的人都不能成功购买。100都抢了却只卖出1件,出现库存遗留问题。
解决方案:通过LUA脚本。LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些redis 事务性的操作。
通过 lua 脚本解决争抢问题,实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题
5. 持久化操作
Redis正常是在内存上运行的,将内存的数据写入磁盘的操作就是持久化操作
Redis提供两种持久化操作
RDB(Redis DataBase)
AOF(Append of File)
RDB
可以用save命令或bgsave命令
在指定的时间间隔内将数据集快照写入磁盘,如果需要恢复数据,只需将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可
save
命令:该命令将在 redis 安装目录中创建dump.rdb文件。会导致工作线程的阻塞
BGSAVE
命令在后台执行。异步。执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。
- 过程
Redis会fork一个子进程来进行持久化,先将数据写入到临时文件中,待持久化过程结束,就用这个临时文件替换上次持久化好的文件,整个过程主进程是不进行任何 IO 操作的。
fork的作用是复制一个与当前进程一样的进程作为子进程,处于效率考虑,Linux引入写时复制技术,一般父进程和子进程共用一段物理内存,只有进程内容发生变化时(插入删除修改)才复制内容给子进程。
- 特点:最后一次持久化的数据可能丢失
一些设置:
rdbchecksum:Redis无法写入时,关闭
rdbcompression :是否压缩,redis 会采用LZF 算法进行压缩。
rdbchecksum:完整性检查
AOF
以日志的形式记录每个写操作,将写操作追加到文件后面,恢复时从头执行一遍日志文件。(与MySQL的binLog类似?)
- 过程:
先将写命令追加到AOF缓冲区;
AOF缓冲区根据AOF持久化策略(always,everysec,no)将操作同步到AOF文件(appendonly.aof
)中
AOF文件大小超过重写策略或手动重写时,会对AOF文件执行rewrite操作,压缩AOF文件容量
Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的
- 同步评率
appendfsync always 每次写入都立即记入日志,性能差但完整性好
appendfsync everysec (默认)每秒记入一次,足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据
appendfsync no 不主动同步,将同步机制交给操作系统
-
AOF默认不开启,如果AOF和RDB同时开启,系统会默认读取AOF的数据
-
rewrite机制
bgrewriteaof
AOF采用文件追加的方式,因此文件会越来越大,Redis会记录上次重写时的AOF大小,默认达到上次重写大小的一倍(且大于64M)时进行重写。
重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制:
- bgrewriteaof触发重写,判断是否当前有 bgsave(不能同时进行,这可以防止两个 Redis 后台进程同时对磁盘进行大量的 I/O 操作) 或 bgrewriteaof 在运行,如果有,则等待该命令结束后再继续执行
- Redis 执行 fork() ,创建子进程
- 子进程将Redis内存中的数据写到临时文件
- 在重写期间,新的写请求同时写入aof_buf缓冲区和aof_rewrite_buf缓冲区(一个内存缓冲区一个原本aof缓冲区,保证原有的aof文件的完整性)
- 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存( aof_rewrite_buf)中的所有数据追加到新 AOF 文件的末尾。
- 新AOF替代原有的AOF文件
对比
RDB文件紧凑节省磁盘空间,适合大数据文件恢复,丢失数据风险大
AOF数据的完整性更高,文件可读,可以处理误操作;占用更多的磁盘空间,恢复速度慢。
推荐都使用
如果对数据不敏感,可以选单独用 RDB。
不建议单独用 AOF,因为可能会出现 Bug。
如果只是做纯内存缓存,可以都不用。
6. 主从复制
一般为一个主机master多个从机slave,主机用于写操作,从机用于读操作,主机的数据会自动备份到从机,这样将读写分离,性能提高,且容灾快速恢复。
模拟操作:设置redis6379.conf,redis6380.conf,redis6381.conf
include /myredis/redis.conf //config内先include公共的文件
daemonize yes
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
>> slaveof <ip><port> 成为某个实例的从服务器
>> info replication 打印主从复制的相关信息
-
主机挂掉,从机待命,依旧记录主机信息,主机重启后依然是主机
-
从机挂掉,重启后就没有主从信息了,需要重新设置slaveof,复制主机全部信息。但如果将配置加到配置文件中,会永久生效
-
一个 Slave 可以是下一个 slave 的 Master (可以套娃),风险是一旦某个 slave 宕机,后面的 slave 都没法备份
-
当一个 master 宕机后,后面的 slave 用
slaveof no one
将从机变为主机,其后面的 slave 不用做任何修改 -
哨兵模式:在后台检测主服务器是否挂掉,如主服务器故障,则自动从从服务器中选择一个作为主服务器,原先的主服务器如果恢复只能做从服务器。
-
选择条件依次为:1)选择优先级靠前的。 2)选择偏移量最大的(获得主机数据最全的) 3)选择runid最小的(随机生成的)
-
//新建 sentinel.conf 文件 sentinel monitor mymaster 127.0.0.1 6379 1 //其中 mymaster 为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量 redis-sentinel /myredis/sentinel.conf //启动哨兵
7. 集群
用于扩容和分担并发写操作。Redis3.0中提供了无中心化的集群配置。
- 集群至少有三个主机,每个主机至少一个从机,数据会分摊在N个主机上。N个主机分摊16384 个插槽(hash slot),集群使用公式CRC16(key) % 16384 来计算键 key 属于哪个槽。因为是无中心的,可以由任一主机进入集群,任意主机之间也可以转换。
- 主机如果挂掉,其中一个从机自动变为主机,主机恢复后变为从机
设置:
1) 创建6个实例,修改配置文件
include /home/bigdata/redis.conf
port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
dir "/home/bigdata/redis_cluster"
logfile "/home/bigdata/redis_cluster/redis_err_6379.log"
cluster-enabled yes 打开集群模式
cluster-config-file nodes-6379.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换
2)启动6个服务,确定所有实例启动,且生成对应的节点配置文件
3)将6个节点合成一个集群
注意,该操作用到一个环境,需要进入cd /opt/redis-6.2.1/src目录下执行以下操作,并且需要采用真实IP
redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379
192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389
192.168.11.101:6390 192.168.11.101:6391
4)以集群的方式登录,设置数据会自动切换到相应的写主机
redis-cli -c -p 6379
其他
cluster nodes 查看集群信息
CLUSTER GETKEYSINSLOT <slot><count> 返回 count 个 slot 槽中的键。(需要在槽对应的实例上操作)
如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage
为 yes ,那么 ,整个集群都挂掉如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage
为 no ,那么,该插槽数据全都不能使用,也无法存储
8. 缓存穿透、缓存击穿、缓存雪崩
1)缓存穿透
- 服务器压力突然增大出现的情况。一般正常情况是客户端请求服务器,服务器先从缓存(Redis)中找数据,没有数据时再到数据库中访问,当服务器压力增大,Redis命中率降低(多数情况下式黑客恶意攻击,发送没有的key),一直无法从缓存中获取数据,而是一直访问数据库,造成数据库瘫痪。
比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
- 解决办法:
- 对不存在的数据缓存。处于容错考虑,如果数据库中查不到的数据不写入缓存,这就导致每次查不到都去找数据库,可以暂时设置对null进行缓存。
- 设置可访问的白名单。使用bitmaps类型定义一个可访问的id白名单,每次访问和 bitmap 里面的 id 进行比较,决定是否拦截。
- 布隆过滤器。实际上是一个很长的二进制向量(位图)和一系列的随机映射色函数(哈希)。布隆过滤器可以检索一个元素是否在一个集合中。空间和查询效率高,但有一定的误识别率和删除困难。可以将所有可能的数据存到一个足够大的bitmaps,不存在的数据就会被过滤。
- 进行实时监控,监控Redis的命中率。
2)缓存击穿
- 情况:Redis的某个Key过期(注意不是大量),同时又大量访问这个key,造成数据库访问压力瞬时增加。
这种情况下redis是正常运行的
- 解决办法:
- 预先设置热门key
- 实时将热门key的过期时长增加
- 使用锁。当在Redis中查不到时,先设置一个排它锁再进行 load db 的操作,如果load db操作失败,说明有其他线程在load,睡眠一段时间后重新查询缓存
3)缓存雪崩
-
情况:同一时间Redis中的大量key过期。这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。
-
解决方法:
- 构建多级缓存架构
- 使用锁或队列,保证不会有大量线程同时操作数据库,但是不适合高并发情况
- 设置过期标志更新缓存。根据该标志更新缓存
- 将缓存失效时间分散开,避免同时失效
9. 分布式锁
- 概念:
在单机中可以通过加锁来保证操作顺序执行,但是在分布式集群中,在一台机器上加锁,其他机器并不能被限制,因此需要一个跨 JVM 的互斥机制来控制共享资源的访问,来解决分布式的并发问题。包括用数据库实现、Redis实现、Zookeeper实现。
- 实现
使用Setnx命令,同时使用expire命令设置过期时间。
>> setnx user 10
>> expire user 15
>> del user 释放锁
由于这是两步操作,如果在setnx命令结束时服务器故障,那么锁就无法释放
>> set user 10 nx ex 15
- 问题一:锁误删
当a,b,c同时争夺一把锁:a抢到后上锁,进行操作,这时a的服务器卡顿,超过锁的过期时间,锁自动释放。b就获得了锁进行操作。当a服务器正常运行,执行完操作后,a 会手动释放锁(机制),这时a 释放的是b的锁
解决方法:获取锁时指定唯一uuid,释放时对比uuid判断是否是自己的锁
- 问题二:删除操作缺乏原子性。
删除操作有两步:1)先比较uuid是否一致 2)执行删除操作
当1)结束时,此时恰好锁到期释放了,就会有别的获得这把锁,再执行2)操作,会把别人的锁释放。
解决办法:LUA脚本具有原子性,使用LUA脚本执行锁的删除操作
- 分布式锁需要满足:
- 互斥性
- 不发生死锁
- 加锁解锁是同一客户端
- 加锁解锁必须具有原子性