redis高级
redis事务
redis事务的本质:一组命令的集合,一个事务中所有命令都会被序列化,事务执行的过程中,会按照顺序执行!
一次性,顺序性,排他性,执行命令!
redis单条命令是保证原子性的,但是事务不保证原子性。
redis事务没有隔离性
redis事务中,所有命令不是直接执行,只有发起执行命令的时候才会执行。
事务执行三个阶段:
- 开启事务(multi)
- 命令入队(…)
- 执行,取消事务(exec,discard)
一组事务执行或者取消后,就已经结束了,下次使用必须要从开启事务开始
例子
# 开启事务
multi
# 命令入列
set key1 k1
set key2 k2
get key1
# 事务执行/取消事务
exec/discard
监控 watch实现redis乐观锁
锁类型:
- 悲观锁
效率低下,做什么都加锁
- 乐观锁
效率高,视情况加锁
获取version,修改的时候比较version
模拟一个场景:a监听某个key并且开启了事务没有执行,b事务也在监听这个key,b中执行了事务,那么a未执行的事务将不会执行成功。
要注意:在事务中是不支持watch监听key的,所以要在事务的外面执行,乐观锁一定要在事务开启之前加上(watch key)
a中
,监听key且事务中命令入列,先不执行
set key 1000
watch key
MULTI
DECRBY key 200
get key
新开一个redis连接,b
中,对这个key进行修改
DECRBY key 200
a
中,再执行
exec
结果为
(nil)
例子说明:A和B秒杀商品,现在只剩一件了,A在浏览这个商品准备下单,B直接下单了,这时候A再下单发现商品已经没了。
Java使用原生Jedis实现事务
是官方推荐的java连接redis的开发工具,导入依赖,jedis
依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.3</version>
</dependency>
简单实现
public static void main(String[] args) {
Jedis j=new Jedis("127.0.0.1",6379);
Transaction t = j.multi();
try{
t.set("user","1");
t.exec();
} catch(Exception e){
t.discard();
} finally {
j.close();
}
}
redis.conf配置文件详解
- 对unit单位大小写,不敏感
- 配置文件中可用
include
引入其他配置文件 bind
绑定ip,指定哪个ip可以访问redis,注释掉后表示所有ip可访问protected-mode
保护模式,值yes/no
,是否开启保护模式(开启后不支持远程访问)daemonize yes
,取值yes/no,是否以守护进程方式运行pidfile /var/run/redis_6379.pid
,如果以守护进程方式运行,则需要指定pidfileloglevel notice
日志级别,默认notice,生产相关,一般不用修改logfile
,日志输出文件路径- 快照
900s内,执行1次修改操作,进行持久化,其他策略略同
save 900 1
save 300 10
save 60 10000
在规定时间内,执行多少次操作,会把数据持久化到.rdb,.aof文件中
rdbcompression yes
rdb文件进行持久化(默认,会消耗一些cpu资源)dbfilename dump.rdb
rdp持久化文件名requirepass 111111
设置redis密码maxmemory-policy
内存到上限之后处理策略appendonly no
持久化AOF模式,默认no,默认使用rdb持久化方式,足够appendfilename
持久化文件名appendfsync everysec
每秒执行一次持久化数据no-appendfsync-on-rewrite no
,重写机制
redis持久化之RDB(Redis DataBase)
执行原理
redis会单独创建(fork)一个子进程来进行数据的持久化,会先将数据写入到一个临时文件中,临时文件持久化结束后,再去替换掉上次持久化的文件。整个进程中,主进程不进行任何io操作,确保了极高的性能,缺点是最后一次持久化可能会造成数据丢失。
触发机制
触发redis持久化机制后,会产生rdb文件
- 配置文件中,满足save规则时,触发
- flushall命令,清除所有数据库数据时触发
- 关闭redis服务时,触发rdb
- 手动bgsave命令
恢复持久化数据
只需要把备份文件dump.rdb放入redis.conf配置文件同级目录下即可
redis启动时会去读取配置文件,由于配置文件指定了dump.rdb文件路径,redis会去自动扫描dump.rdb中的数据,并且进行恢复。
RDB优缺点
优:
- 适用于大量数据的恢复
- 对数据完整性要求不高
缺:
- redis宕机,可能导致最后一次修改的数据丢失
- fork进程执行的时候,可能占用一定的空间
持久化之AOF(Append Only File)
redis默认使用rdb的持久化方式,但是如果aof模式开启了,那么会去同时使用rdb和aof作为持久化,但是数据恢复时只会执行aof来恢复数据(aof模式的数据完整性更高)
执行原理
实现方式,fork一个子进程,去记录每次执行的命令追加到aof文件中,恢复数据时,直接执行aof文件来恢复数据
appendonly yes 即可开启aof模式
appendfilename appendonly.aof 指定文件名
当aof文件损坏时
假如aof文件损坏了(模拟:在aof文件中随便加了一些字符),那么启动redis时就会失败,由于aof文件损坏,redis启动时读取aof文件时出现问题,造成无法启动redis服务
解决方案:
使用redis目录下redis-check-aof工具修复aof文件
redis-check-aof --fix appendonly.aof
AOF优缺点
优:
- 每一次修改都会同步(命令记录入aof文件),数据完整性更高
- 每秒同步一次,只丢失一秒的数据
- 不开启时,从不同步,效率最高
缺:
- aof文件远大于rdb文件,修复速度也比rdb文件慢很多
- aof运行效率比rdb慢
redis发布订阅
redis发布订阅(pub/sub),是一种消息通信模式,发送者发送(pub)消息,订阅者订阅(sub)消息
redis客户端可以订阅任意数量的频道
三个角色
- 消息生产者
- 频道
- 消息订阅者
订阅者订阅频道,订阅后会自动监听
# 订阅一个频道wlhme
subscribe wlhme
发布者发布消息到频道
publish wlhme helloworld
Redis主从复制
指将一台服务器的数据复制给其他服务器,前者是主机,后者从机,数据复制是单向的,只能是从主机到从机。master以写为主,slave以读为主。
主从复制,读写分离。减缓服务器压力,一主二从。
作用:
- 数据冗余,主从复制实现了数据备份
- 故障恢复,主节点出问题,从节点可以提供服务,实现快速故障修复
- 负载均衡,主从复制基础上,配合读写分离,分担服务器负载,提高redis并发量
- 高可用基石:主从复制是哨兵模式和集群的基础
单台redis服务器,最大使用内存不应超过20G,超了就整从机。
默认情况下,每台redis服务器都是主节点,可以修改配置文件变成从节点。一个主节点可以有多个从节点,但是一个从节点只能有一个主节点。
Redis集群
环境配置:只需要配置从节点,主节点不用配置。
# 查看集群配置信息
info replication
从节点如何配置?
-
方案一:使用命令方式配置(不是永久的)
# 成为本机6379端口服务的从机(认主) slaveof 127.0.0.1 6379
要点:
如果主节点存在密码,那么需要在从机配置文件中中加上masterauth 主密码,配置主节点的密码才可以。
-
方案二:直接在从节点配置文件中加上主节点的配置
replicaof 127.0.0.1 6379 masterauth 111111
细节
主写从读。主节点存入数据后,复制数据到从节点,从节点可以读到数据。
从机只能读数据,不能写入数据。
主节点宕机后,从节点仍然可以提供服务,过一段时间主节点上线,从节点仍然可以和主节点保持连接(获取到主机写的数据)。
如果是使用命令行配置的从机,那么从机挂掉后再启动就会变成主机(最好配置在文件中,可以永久保存),当从机再次连接到主机时,主机的数据仍会同步到从机中,仍然可以读取到主机中的数据。
主从复制的实现原理
slave启动服务,连接到master后会发送一个sync同步命令。master接到命令,启动后台存盘进程,同时收集所有用于修改数据的命令,后台进程执行完毕以后,master将整个数据文件发送到slave,完成同步。
- 全量复制
slave接收到master的数据文件后,直接同步数据加载到内存中。
- 增量复制
master继续将修改数据命令依次发送给slave,实际上就是,master和slave保持连接时,master修改一条数据,slave同步一次数据。
要点:
只要是重新连接master,那么全量复制将自动执行。
哨兵模式
可参考文章
手动谋权篡位过于费时费力,采用自动选举老大的哨兵模式。
哨兵模式是一种特殊模式,redis提供了哨兵模式的命令,哨兵作为独立的进程执行,原理:哨兵通过发送命令到redis服务器,等待redis服务器响应,从而监控运行的多个redis实例。
当主节点挂掉后,哨兵1先发现主服务不可用(主观下线),后面的哨兵也发现了,并且达到一定数量后,哨兵们会进行投票选举。选举完成后,通过发布订阅模式,让各个哨兵通知自己监控的从节点切换主节点(换老大),这个过程是客观下线。
实现
在redis目录下,创建sentinel.conf配置文件
内容
# 监控本机6379的redis,投票机制是1
sentinel monitor red1 127.0.0.1 6379 1
启动哨兵
redis-sentinel /sentinel.conf
当我们的redis主节点挂了之后,哨兵会选举老大,使之成为主节点。当原主节点恢复了,他也只能当从机!!!
优点
- 基于主从复制模式,所有主从配置优点它都有
- 主从可以切换,故障可以转移,可用性好
- 主从模式的升级,手动改为自动选举老大
缺点
- redis不好在线扩容,集群容量一旦达到上限,在线扩容非常麻烦
- 实现哨兵模式配置很麻烦,里面有很多选择
缓存穿透和雪崩
缓存穿透(数据找不到)
用户发起请求查询一个数据,发现redis中没有,也就是缓存没有命中,于是去mysql中查询,但是持久化数据库也没有数据,于是本次查询失败。当用户量很多,都去发起请求,都没有缓存命中,都去请求持久化数据库,给持久层数据库造成很大的压力,就是缓存穿透。
解决方案
- 布隆过滤器
- 缓存空对象
当缓存未命中,且持久层数据库也无数据,就在缓存中加一个空对象返回
缓存击穿(热点key过期)
指一个key非常热点,扛着大量的请求压力,并发集中对一个点进行访问。放这个key在失效的瞬间,持续的大并发击穿缓存,直接访问持久层数据库,给数据库造成很大压力。
解决方案
- 设置热点数据永不过期
- 加互斥锁
分布式锁,保证每个key每次只有一个线程去进行访问
缓存雪崩(大量key同时过期)
指在某一时间段,缓存key集中失效,或者redis突然宕机。
所有的访问压力全部落在了持久层数据库的身上,造成很大的访问压力。
解决方案
- redis集群
- 限流降级
方案思想:redis的大部分key失效后,进行加锁或者队列,限制持久层数据库的访问线程数量
- 数据预热
正式部署之前,先访问一遍,把可能大量访问的数据加载到缓存中,在大量访问前,手动加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
使用watch实现秒杀相关业务代码
String redisKey = "miaosha";
ExecutorService executorService = Executors.newFixedThreadPool(20);//20个线程
try {//初始化
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 初始值
jedis.set(redisKey, "0");
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {//尝试1000次
executorService.execute(() -> {
try (Jedis jedis1 = new Jedis("127.0.0.1", 6379)) {
jedis1.watch(redisKey);
String redisValue = jedis1.get(redisKey);
int valInteger = Integer.valueOf(redisValue);
String userInfo = UUID.randomUUID().toString();
// 没有秒完
if (valInteger < 20) {//redisKey
Transaction tx = jedis1.multi();//开启事务
tx.incr(redisKey);//自增
List list = tx.exec();//提交事务,如果返回nil则说明执行失败,因为我watch了的,只要执行失败,则
// 进来发现东西还有,秒杀成功
if (list != null && list.size() > 0) {
System.out.println("用户:" + userInfo + ",秒杀成功!当前成功人数:" + (valInteger + 1));
} else {//执行结果不是OK,说明被修改了,被别人抢了
System.out.println("用户:" + userInfo + ",秒杀失败");
}
} else {//东西秒完了
System.out.println("已经有20人秒杀成功,秒杀结束");
}
} catch (Exception e) {
e.printStackTrace();
}//关闭redis
});
}
executorService.shutdown();//关闭线程池