## Redis教程由入门到精通 ### 一、Redis持久化操作 #### 1.1 前言 在项目中使用Redis做缓存,方便多个业务进程之间共享数据。由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能,将数据保存到磁盘上,当redis重启后,可以从磁盘中恢复数据。 redis提供两种方式进行持久化 一种是RDB (Redis DataBase)持久化(原理是将Reids在内存中的数据库记录定时,(时间间隔)[dump](https://so.csdn.net/so/search?q=dump)到磁盘上的RDB持久化) 另外一种是AOF (Append of File)持久化(原理是将Reids的操作日志以追加的方式写入文件)。 那么这两种持久化方式有什么区别呢,该如何选择呢? #### 1.2 二者的区别 RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。 AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。 #### 1.3 二者优缺点 ##### 1.3.1 RDB存在哪些优势呢? 1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。 2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。 3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。 4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。 save命令可以主动触发保存操作,但是它是由主进程来完成的 bgsave命令,是由子进程完成的 ##### 1.3.2 RDB又存在哪些劣势呢? 1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。 2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。 ##### 1.3.3 AOF的优势有哪些呢? 1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3种同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。 2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心。 ##### 1.3.4 AOF的劣势有哪些呢? 1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。 2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。 二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能。 #### 1.4 RDB持久化配置 Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息: ~~~sh dir ./ #数据库存储的位置 dbfilename news.rdb #rdb数据库文件名字,可以重命名 save 900 1 #在900秒(15分钟)之内,如果至少有1个key发生变化,则dump内存快照。 save 300 10 #在300秒(5分钟)之内,如果至少有10个key发生变化,则dump内存快照。 save 60 10000 #在60秒(1分钟)之内,如果至少有10000个key发生变化,则dump内存快照。 save "" #关闭rdb之前需要先注释掉 save 900 1 ...等其他操作 ~~~ #### 1.5 AOF持久化配置 ~~~sh appendonly no #改为yes appendfilename "appendonly.aof" #数据库文件 ~~~ 在Redis的配置文件中存在三种同步方式,它们分别是: ~~~sh #appendfsync always #每次有数据修改发生时都会写入AOF文件。 appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。 #appendfsync no #从不同步。高效但是数据不会被持久化。 ~~~ ### 二、 Redis事务和锁机制 #### 2.1 事务定义 Redis事务是一个单独的隔离操作,事务中的所有命令都会序列化,按顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断。 Redis事务的主要作用就是串联多个命令防止别的命令插队。 三个基本命令:multi、exec、discard multi代表组队阶段,exec代表执行阶段,组队的过程可以通过discard来放弃组队。 从输入multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入exec后,Redis会将之前的命令队列中的命令依次执行。 组队的时候的命令发送错误,执行都不会成功。如果组队的时候都成功,执行的时候哪个失败,就不计算哪个,这个和mysql的事务区别很大。 #### 2.2 事务特性 ##### 2.2.1 单独的隔离操作 事务中所有命令都会被序列化,按顺序执行。事务在执行过程中,不会被其他客户端发来的命令请求所打断。 ##### 2.2.2 没有隔离级别的概念 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行 ##### 2.2.3 不保证原子性 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。 #### 2.3 事务的示例 事务1:正常组队,执行exec提交 ~~~sh multi set k1 v1 set k2 v2 exec ~~~ 事务2:discard放弃组队 ~~~sh multi set k1 v1 set k2 v2 discard ~~~ 事务3:组队阶段语句错误,导致exec执行失败 ~~~sh multi set k1 v1 set k2 v2 set k3 exec ~~~ 事务4:组队阶段正常,exec执行失败 ~~~sh multi set k1 v1 set incr v1 set k2 v2 exec ~~~ #### 2.4 悲观锁与乐观锁 锁的作用:事务冲突的问题,很多请求共同修改一个数据导致的冲突怎么解决? 可以使用锁来解决。Redis中,锁机制可以分为两种: 1、悲观锁 顾名思义,就是很悲观,每次去操作的时候都认为别人会修改数据,所以每次操作数据的时候都会上锁,这样别人就不能操作了,直到他操作完,别人才可以进行操作。特别类似于生活中的ATM机取款,别人进去取钱都会先关门,直到别人取完钱出来,你才可以进去取。这种操作方式,效率极其低下。 2、乐观锁 顾名思义,就是很乐观,每次去操作的时候都认为别人不会修改数据,所以不会上锁,但是一般在一开始会在数据上加一个数据的版本号,所有人都可以得到v1.0的数据。不管谁先拿到数据后,都先会和数据进行版本的比对,如果版本号一致就可以操作,操作完数据后,都会修改版本号为v2.0。乐观锁适用于多读的应用类型,这样可以提高吞吐量,Redis是利用check-and-set机制实现事务的。 Redis的Check-and-Set(CAS)操作是一种乐观锁实现,用于解决分布式系统中的数据竞争问题。CAS操作包括三个参数:需要更新的键(key),原始版本号 (oldValue),以及新的版本号(newValue)。Redis会检查键的当前版本号是否与提供的原始版本号匹配,如果匹配,则执行更新操作。在Redis中,实现CAS的命令 是`GETSET`。这个命令会设置键的值,并返回键的旧值。如果旧值与预期的版本号不匹配,则操作失败。 #### 2.5 乐观锁的使用 命令: ~~~ WATCH key ~~~ 在执行multi之前,先执行WATCH key1 [key2],可以监视一个或多个key,如果在事务执行之前这个或这些key被其他命令所改动,那么事务将被打断。 示例: ~~~shell set num 100 watch num multi set log test01 decr num exec ~~~ 在此组队期间,在xshell复制一个ssh渠道,输入命令 ~~~ incr num ~~~ 模拟其他请求更改了数据,测试乐观锁会不会生效 ### 三、事务和锁机制实现秒杀案例 10个库存、100个人来秒杀 正常情况:90个人买不到,只有10个人才能秒杀到 商品库存清单 | key | String | | ---- | -------- | | kc | 剩余个数 | 秒杀成功者清单 | key | set | | ---- | ------------------------------------------------------------ | | user | 成功者的userid1 成功者的userid2 成功者的userid3...成功者的userid10 | 后台代码实现 导入依赖: ~~~xml <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> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ~~~ controller ~~~java package cn.hxzy.controller; import cn.hxzy.common.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Objects; import java.util.Random; @CrossOrigin(originPatterns="http://192.168.0.62:8848",allowCredentials = "true") @RestController public class UserController { @Autowired private StringRedisTemplate stringRedisTemplate; //秒杀的方法 @PostMapping("/buy") public Result buy() { //定义一个用户的随机编号 Long id = Math.round(Math.random() * 1000); //每个请求都有userId String uid = id.toString(); //101 (获取的是当前登录的用户的编号) //1、判断如果库存为null,则证明活动还没有开始 String kc = stringRedisTemplate.opsForValue().get("kc"); if (Objects.isNull(kc)) { return new Result(-2, "活动还未开始,请耐心等待"); } //2、判断当前用户是否购买过 Boolean isBuy = stringRedisTemplate.opsForSet().isMember("user", uid); if (isBuy) { return new Result(-3, "已经购买成功,请勿重复购买"); } //3、判断如果商品数量,库存数<=0,秒杀结束 if (Integer.parseInt(kc) == 0) { return new Result(-4, "秒杀活动已结束"); } //4、把秒杀成功的用户添加到秒杀清单中去 Long num = stringRedisTemplate.opsForSet().add("user", uid); if (num > 0) { //5、购买商品 stringRedisTemplate.opsForValue().decrement("kc", 1); return new Result(0, "秒杀成功!"); } else { throw new RuntimeException("系统异常!"); } } } ~~~ 返回类 ~~~java package cn.hxzy.common; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Result { private Integer code; private String msg; } ~~~ 前台html ~~~html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title></title> </head> <body> <div id="app"> <span>Apple 苹果15promax A3108 </span><br/> <span>原价:¥14999.00 </span><br/> <span>秒杀价¥1.00元</span><br/> <input type="button" value="秒杀" @click="buyGood" v-show="isShow"/><br/> <span >{{message}}</span> </div> </body> <script src="https://cdn.staticfile.net/vue/2.7.0/vue.min.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> axios.default.withCredentials = true new Vue({ el: "#app", data: { message:"", isShow:true }, methods: { buyGood() { axios({ method: "post", url: "http://192.168.0.62:8080/buy" }).then(res => { this.message=res.data.message if(res.data.code==-4){ this.isShow=false } }) } } }) </script> </html> ~~~ #### 3.1 ab 工具模拟并发 centos7需要手动安装ab工具,来进行模拟多人并发操作:压测工具 ~~~bash yum install httpd-tools ~~~ 通过浏览器测试 -n代表请求的次数 -c代表当前的并发次数 使用如下命令测试即可: ~~~bash ab -n 1000 -c 100 http://192.168.0.62:8080/buy ~~~ 注意:redis安装在阿里云,项目是在本地,访问不通。 或者使用jmeter工具模拟高并发进行压力测试 #### 3.2 超卖问题解决 添加事务的控制,SessionCallback接口是一个批处理的接口,通过这个接口就可以把批量的命令放到redis的一个连接去执行。 ~~~java package cn.hxzy.controller; import cn.hxzy.common.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.SessionCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Objects; @CrossOrigin(originPatterns="http://192.168.0.62:8848",allowCredentials = "true") @RestController public class User1Controller { @Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("/buy") public Result buy() { Result result = new Result(); //定义一个用户的随机编号 Long id = Math.round(Math.random() * 1000); String uid = id.toString(); //SessionCallback是一个函数式接口 SessionCallback sessionCallback = new SessionCallback() { @Override public Object execute(RedisOperations redisOperations) throws DataAccessException { stringRedisTemplate.watch("kc"); //添加乐观锁,监控kc这个key的变化 stringRedisTemplate.setEnableTransactionSupport(true);//开启事务 //1、判断如果库存为null,则证明活动还没有开始 String kc = stringRedisTemplate.opsForValue().get("kc"); if (Objects.isNull(kc)) { result.setCode(-2); result.setMsg("活动还未开始,请耐心等"); return null; } //2、判断当前用户是否购买过 Boolean isBuy = stringRedisTemplate.opsForSet().isMember("user", uid); if (isBuy) { result.setCode(-3); result.setMsg("已经购买成功,请勿重复购买"); return null; } //3、判断如果商品数量,库存数<=0,秒杀结束 if (Integer.parseInt(kc) <= 0) { result.setCode(-4); result.setMsg("秒杀活动已结束"); return null; } //4、开启事务的组队 stringRedisTemplate.multi(); //4.1 第一个命令 库存减1 stringRedisTemplate.opsForValue().decrement("kc", 1); //4.2 把购买成功的用户ID添加到set集合中 stringRedisTemplate.opsForSet().add("user", uid); //5、执行事务 List<Object> list = stringRedisTemplate.exec(); if (list.size() > 0) { result.setCode(0); result.setMsg("秒杀成功"); //6、返回执行的结果 return list; } else { result.setCode(-1); result.setMsg("发生异常"); return null; } } }; stringRedisTemplate.execute(sessionCallback); return result; } } ~~~ ### 四 应用问题解决 设计Web高并发项目,可能要考虑的问题就是:缓存穿透、缓存击穿与缓存雪崩。 #### 1 缓存穿透 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。  在流量大时,可能数据库就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。 如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。 **解决方案:** 1)最基本的方式就是做好参数校检,**接口层增加校验**,如用户鉴权校验,id做基础校验,id<=0的直接拦截; 2)缓存无效的key,如果数据库和缓存都找不到某个key的数据,就直接写一个到redis中并设置它的过期时间 set key value EX 10000。这种方式可以解决请求的key变化不频繁的情况,如果遇到专门的黑客攻击就不能解决这个情况。但是如果依然想用这个方法的话,那么在设置过期时间的时候,时间短一点,比如是一分钟。多说一句设置key的格式一般是:表名:列名:主键名:主键。 3)设置可访问的IP白名单,设置哪些IP地址可以访问,哪些不可以访问 4)当发现Redis的命中率急速降低,需要排查访问对象和访问的数据,当发现访问的都是无效的垃圾数据时,(一般出现此种情况,就是在遭受黑客攻击),可以设置黑名单限制访问,同时配合网站运维人员联合网警去解决问题。 #### 2 缓存击穿 缓存击穿:现象是数据库访问压力瞬时增加很多, redis中并没有出现大量的key过期,redis还是正常运行的 ,但是数据库已经崩溃了。 原因是:redis中某个key过期了,大量访问使用这个key,一般这个key就是一个热门的key  **解决方案:** 1、预先设置热点数据永远不过期。 2、实时调整现场监控哪些数据热门,实时调整key的过期时长。 3、接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些服务不可用时候,进行熔断,失败快速返回机制。 #### 3 缓存雪崩 缓存雪崩现象:数据库压力过大,应用程序反应时间变慢,造成redis里面大量的应用等待,造成数据库崩溃了,应用程序崩溃了,redis也崩溃了。 原因:在极少的时间内,出现了大批量的key的集中过期情况,访问不到redis,就去数据库访问,从而引发雪崩问题。  缓存失效的雪崩效应对底层系统的冲击非常可怕 **解决方案**: 1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。比如我们可以在原有的失效时间基础上再增加一个随机数,比如1-5分钟的随机,这样 每个缓存过期的重复率就会降低,就很难引发集体失效的事件。 2. 提前预警:设置过期标志更新缓存。记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。 ### 五、Redis的应用场景 Redis是一种内存中的数据结构存储系统,常用作数据库、缓存和消息代理。它的主要应用场景包括: - 缓存。Redis可以存储频繁访问的数据,如数据库中的热点数据,以减少对数据库的访问,提高应用程序的响应速度。 - 会话存储。用于在无状态的服务器之间共享用户相关的状态数据,如用户的会话信息。 - 分布式锁。在多个节点操作共享资源时,使用Redis作为分布式锁来协调操作。 - 速率限制。通过Redis的计数器功能实现速率限制,如限制用户的请求频率。 - 排行榜。利用Redis的排序集合功能实现各种排行榜。 - 计数器。记录文章的阅读量、微博点赞数等,并可定时同步到数据库。 - 消息队列。Redis支持发布/订阅模型,可用于实现简单的消息队列功能。 - 统计访问次数。使用Redis的incr命令来增加访问次数的计数。 此外,Redis还适用于大型电商网站、视频直播和游戏应用等场景,通过提供高速的数据访问,减少数据库负载,提高系统效率。