《Redis:NoSQL演进之路与Redis深度实践解析》(一)+https://developer.aliyun.com/article/1625021
Hyperloglog命令
网页的访问,一个人访问一个网站多次,但是还是算作一个人!
如果允许容错,那么一定使用 Hyperloglog
。
127.0.0.1:6379> pfadd mykey a b c d e f g h i j#创建第一组元素 mykey (integer) 1 127.0.0.1:6379> pfcount mykey#统计mykey元素的基数数量 (integer) 10 127.0.0.1:6379> pfadd mykey2 i j z x c v b n m#创建第二组元素mykey2 (integer) 1 127.0.0.1:6379> pfcount mykey2 (integer) 9 127.0.0.1:6379> pfmerge mykey3 mykey mykey2合并两组mykey mykey2=》mykey3并集 OK 127.0.0.1:6379> pfcount mykey3#看并集的数量! (integer) 15 127.0.0.1:6379>
Bitmap命令
位存储
统计用户信息、活跃,不活跃;登录,未登录;打卡,未打卡!两个状态的场景,都可以用Bitmaps!
Bitmap位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态!
127.0.0.1:6379> setbit sign 0 1 (integer) 0 127.0.0.1:6379> setbit sign 1 0 (integer) 0 127.0.0.1:6379> setbit sign 2 0 (integer) 0 127.0.0.1:6379> setbit sign 3 1 (integer) 0 127.0.0.1:6379> setbit sign 4 1 (integer) 0 127.0.0.1:6379> setbit sign 5 0 (integer) 0 127.0.0.1:6379> setbit sign 6 0 (integer) 0 127.0.0.1:6379> getbit sign 3#查看某一天是否打卡 (integer) 1 127.0.0.1:6379> getbit sign 6 (integer) 0 127.0.0.1:6379> bitcount sign#统计这周的打卡记录 (integer) 3 127.0.0.1:6379>
以上是用,bitmap来记录周一到周日打卡!
周一:1 周二:0 周三:0 周四:1…
事务
Redis事务的本质:一组命令的集合!一个事务中所有命令都会被序列化,在事务执行过程中,会按照顺序执行!
Redis事务没有没有隔离级别的概念!
Redis单条命令式保存原子性的,但是事务不保证原子性!
Redis的事务:
- 开启事务(multi)
- 命令入队(…)
- 执行事务(exec)
正常执行事务
127.0.0.1:6379> multi #开启事务,就跟点了一个鞭炮一样 OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> get k2 QUEUED 127.0.0.1:6379> set k3 v3 QUEUED 127.0.0.1:6379> exec #执行事务 1) OK 2) OK 3) "v2" 4) OK 127.0.0.1:6379>
放弃事务
127.0.0.1:6379> multi#开启事务 OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> set k4 v4 QUEUED 127.0.0.1:6379> discard#取消事务 OK 127.0.0.1:6379> get k4#事务队列中的命令都不会被执行! (nil) 127.0.0.1:6379>
编译型异常【代码有问题,命令有问题,事务中所有命令不再执行】
127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> set k3 v3 QUEUED 127.0.0.1:6379> getset k3#错误的命令 (error) ERR wrong number of arguments for 'getset' command 127.0.0.1:6379> set k4 v4 QUEUED 127.0.0.1:6379> set k5 v5 QUEUED 127.0.0.1:6379> exec#执行事务报错 (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get k5#所有命令都不会被执行~ (nil) 127.0.0.1:6379>
运行时异常I/O(如果事务队列中存在语法错误,那么执行命令的时候,其他命令可以正常执行,错误命令抛出异常~)
127.0.0.1:6379> set k1 "v1" OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr k1#会执行失败~ QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> set k3 v3 QUEUED 127.0.0.1:6379> get k3 QUEUED 127.0.0.1:6379> exec 1) (error) ERR value is not an integer or out of range#虽然第一条命令执行报错了,但是其他命令依旧正常执行! 2) OK 3) OK 4) "v3" 127.0.0.1:6379> get k2 "v2" 127.0.0.1:6379> get k3 "v3" 127.0.0.1:6379>
总结
Redis支持乐观锁。
悲观锁
- 很悲观,认为什么时候都会出现问题,无论做什么都会加锁
乐观锁
- 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人去修改过这个数据
- 获取Version
- 更新的时候比较version
Jedis
Jedis是Redis官方推出的可以用Java操作Redis的连接开发工具!属于Java操作Redis的中间件。
Ping测试
- 连接数据库
- 操作命令
- 断开连接
<dependencies> <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>5.1.0</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.43</version> </dependency> </dependencies>
package com.linghu; import redis.clients.jedis.Jedis; /** * @author linghu * @date 2024/1/9 11:33 */ public class TestPing { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1",6379); System.out.println(jedis.ping()); } }
常用API
所有的API命令都是上面对应学过的Redis命令,一个都没有变化!
- String
- List
- Set
- Hash
- ZSet
package com.linghu; import org.json.JSONObject; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; /** * @author linghu * @date 2024/1/9 11:33 */ public class TestPing { public static void main(String[] args) { Jedis jedis = new Jedis("127.0.0.1",6379); jedis.flushDB(); JSONObject jsonObject = new JSONObject(); jsonObject.put("hello","world"); jsonObject.put("name","linghu"); //开启事务 Transaction multi = jedis.multi(); String result = jsonObject.toString(); try { multi.set("user1",result); multi.set("user2",result); int i=1/0;//代码抛出异常,执行失败! multi.exec();//执行事务! } catch (Exception e) { multi.discard();//放弃事务 e.printStackTrace(); } finally { System.out.println(jedis.get("user1")); System.out.println(jedis.get("user2")); jedis.close();//关闭连接 } } }
SpringBoot整合
1、导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.7.10</version> </dependency>
2、配置连接
spring.redis.host=127.0.0.1 spring.redis.port=6379
3、测试
package com.linghu; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; /** * @author linghu * @date 2024/1/15 11:02 */ @SpringBootTest(classes = Redis02SpringbootApplicationTest.class) @RunWith(SpringRunner.class) @ActiveProfiles(profiles = {"dev"}) public class Redis02SpringbootApplicationTest { @Resource private RedisTemplate<String, String> redisTemplate; @Test public void contextLoads(){ redisTemplate.opsForValue().set("mykey","关注令狐"); System.out.println(redisTemplate.opsForValue().get("mykey")); } }
Redis持久化
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能。【主从设备中经常放到从设备上】
RDB(Redis DataBase)
什么是RDB?
RDB是Redis的一种数据持久化到磁盘的策略,是一种以内存快照形式保存Redis数据的方式。所谓快照,就是把某一时刻的状态以文件的形式进行全量备份到磁盘,这个快照文件就称为RDB文件,其中RDB是Redis DataBase的缩写。
AOF(Append Only File)
Redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。【不怎么使用】
AOF 保存的是 appendonly.aof文件
优点:
- 每一次修改都同步,文件的完整会更加好!
- 每秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高的!
缺点:
- 相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
- Aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化
Redis发布订阅
Redis发布订阅(pub/sub)是一种消息通信模式:发送者pub发送消息,订阅者sub接收消息。微信,微博,关注系统。
下图展示了频道channel1,以及订阅这个频道的三个客户端-client2、client5和client1之间的关系:
订阅端:
127.0.0.1:6379> subscribe linghu #订阅一个频道linghu Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "linghu" 3) (integer) 1 1) "message" 2) "linghu" 3) "hello,linghu"#收到的推送的消息
发送端:
127.0.0.1:6379> publish linghu "hello,linghu"#发布者发布消息到频道上 (integer) 1 127.0.0.1:6379>
哨兵模式
当主服务器宕机后,需要手动把一台从服务器切换成主服务器,这就需要人工干预,费时费力。这个时候提出了 哨兵模式。
Redis从2.8开始正式提供了Sentinel(哨兵)架构来解决这个问题。
哨兵模式是一种特殊模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是 哨兵通过发送命令,等待Redis服务器响应,从而监控运行多个Redis实例。
哨兵的两个作用:
- 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
- 当哨兵检测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他从服务器,修改配置文件,让他们切换主机。
一个哨兵对Redis服务器进行监控可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
主观下线
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用
客观下线
当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机
优点:
- 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
- 主从可以切换,故障可以转移,系统的可用性就会更好
- 哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
- Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
- 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
Redis缓存穿透和雪崩
目前企业中对Redis缓存技术的运用,如下:
客户端首先请求Redis缓存,查询到了直接获取;查询不到就会去数据库中直接查询。
缓存穿透
用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透 。
解决方案
1、布隆过滤器
我们可以把布隆过滤器理解为一个set集合,我们可以通过add往里面添加元素,通过contains来判断是否包含某个元素。
客户端发送过来的数据会首先进行校验,不符合规则进行丢弃,从而避免了对底层存储层系统的查询压力。
2、缓存空对象
当存储层没有被命中(客户端查询不到数据),即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再进行访问这个数据将会从缓存中获取,保护了后端数据源。
以上两种方法会存在问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞 。
解决方案
1、设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题
2、加互斥锁
使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁 。
缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
解决方案
1、Redis高可用
多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群
2、限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量
3、数据预热
在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。