《Redis:NoSQL演进之路与Redis深度实践解析》(二)

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 《Redis:NoSQL演进之路与Redis深度实践解析》(二)

《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,设置不同的过期时间,让缓存失效的时间点尽量均匀。

目录
相关文章
|
NoSQL 关系型数据库 MySQL
【Redis 系列】redis 学习一,数据库的演进及 Nosql 的初步认知
【Redis 系列】redis 学习一,数据库的演进及 Nosql 的初步认知
|
存储 缓存 监控
Redis 的生态 | 学习笔记
快速学习 Redis 的生态
Redis 的生态 | 学习笔记
|
2月前
|
缓存 NoSQL 关系型数据库
《Redis:NoSQL演进之路与Redis深度实践解析》(一)
《Redis:NoSQL演进之路与Redis深度实践解析》(一)
31 0
|
消息中间件 缓存 运维
Redis核心技术与实战
作为同时具备高性能、高可靠和高可扩展性的典型键值数据库,Redis不仅功能强大,而且稳定,理所当然地成为了大型互联网公司的首选。
305 0
|
存储 缓存 NoSQL
高并发核心技术Redis系列(一)--------概述
任何技术的出现都是一步一步演进出来的。
168 0
高并发核心技术Redis系列(一)--------概述
|
存储 NoSQL 算法
高并发核心技术Redis系列(三)--------基本知识(下)
在我们做站点流量统计的时候一般会统计页面UV(独立访客:unique visitor)和PV(即页面浏览量:page view)。
191 0
高并发核心技术Redis系列(三)--------基本知识(下)
|
存储 NoSQL Java
|
存储 消息中间件 缓存