IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> Redis (一) -> 正文阅读

[大数据]Redis (一)

资料来源:【尚硅谷】Redis 6 入门到精通 超详细 教程

Redis (一)

Redis (二)

一 概述简介

1.1 NoSql介绍

全程为Not Only SQL,泛指非关系型的数据库。

特点:

  • 不遵循SQL标准
  • 不支持ACID
  • 远超于SQL性能

应用场景:用不着SQL和用了SQL也不行的场景

1.2 Redis介绍

  • Redis 是一个开源的 key-value 存储系统
  • Redis数据存在内存中
  • Redis支持持久化
  • Redis支持master-slave (主从) 同步。

1.3 Redis安装

1.3.1 下载源码包

下载安装地址:Download | Redis

1.3.2 执行打包和安装

从源码编译安装:Install Redis from Source | Redis

1、执行make

cd /../redis-6.2.7
make

2、执行make install

cd /../redis-6.2.7
make install

默认会将redis-server 和redis-cli安装在 /usr/local/bin下

1.4 启动与关闭

1.4.1 启动

前台启动:当此窗口关闭,Redis进程即关闭

redis-server

后台启动:以后台进程的方式运行

1、修改redis.conf,允许后台运行:daemonize yes

cd /../redis-6.2.7
vi redis.conf

2、启动时指定redis.conf文件

redis-server redis.conf

1.4.2 关闭

方式一:kill process

方式二:在redis-cli中使用shutdown命令

1.5 相关知识点介绍

默认端口

Redis默认端口是6379,对应MERZ在九宫格键盘上的数字。

16个数据库

Redis有16个数据库,索引【0,15】,默认连接使用的是一个索引为【0】的数据库。

可使用select 进行切换:

单线程+多路复用

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态。比如调用 select 和 poll 函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

二、常用数据类型

2.1 Redis键常用操作

操作

说明

keys *

查看当前库匹配的key

exists key

判断key是否存在

type key

查看key的类型

del key

删除key

unlink key

非阻塞式删除key

expire key 10

设置key过期时间(单位:s)

ttl key

查看key有效期:-1永不过期、-2已过期

select 0

切换库

dbsize

查看数据库键数量

flushdb

清空当前库

flushall

清空所有库

2.2 字符串(string)

最基本的类型,一个key对应一个value

是二进制安全的,string可以包含任何数据,比如jps图片或序列化的对象

一个字符串最大可以是512M。

2.2.1 常用操作

操作

说明

set [key] [value]

赋值

get [key]

取值

append [key] [value]

追加值

strlen [key]

获取长度

setnx [key] [value]

当不存在时设置值

incr [key]

值自增1,仅可对整数操作

decr [key]

值自减1,仅可对整数操作

incrby [key] [step]

值自增step,仅可对整数操作

decrby [key] [step]

值自减step,仅可对整数操作

mset [key1] [value1] [key2] [value2]

批量赋值

mget [key1] [key2]

批量取值

msetnx [key1] [value1] [key2] [value2]

当不存在时设置值,批量赋值。一旦一个存在,所有都失败

getrange [key]?[start]?[end]

获取子字符串。含头也含尾

setrange [key]?[start] [value]

从start处开始赋值

setex [key]?[expire] [value]

赋值&设置有效期

getset [key] [value]

取老值赋新值

2.2.2 存储结构

String 的数据结构为简单动态字符串 (Simple Dynamic String, 缩写 SDS),是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。

需要注意的是字符串最大长度为 512M。

2.3 列表

列表List,一个双向链表。

2.3.1 常用操作

操作

说明

lpush [key] [value] [value]

从左侧添加元素

rpush [key] [value] [value]

从右侧添加元素

lrange [key] [start] [end]

遍历获取元素

lpop [key]

从左侧抛出元素(元素全部删除后该列表也删除)

rpop [key]

从右侧抛出元素(元素全部删除后该列表也删除)

lindex [key] [index]

获取索引位置元素

linsert [key] [before|after] [value] [value]

在指定元素前插入元素(只插入一个)

lrem [key] [count] [value]

从左侧删除元素(删除count个)

lset [key] [index] [value]

设置索引处元素值

2.3.2 数据结构

List的数据结构为快速链表quickList。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

2.4 集合

集合,不重复的元素

2.4.1 常用操作

操作

说明

sadd [set] [value] [value]

集合添加添加元素

smembers [set]

全部元素

sismember [set] value]

判断元素是否在集合中

scard [set]

集合数量

srem [set] value]

删除元素(元素全部删除后该集合也删除)

spop [set]

随机抛出元素

srandmember [set] [count]

随机获取元素

sinter [set1] [set2] [set3]

获取交集

sunion [set1] [set2] [set3]

获取并集

sdiff [set1] [set2] [set3]

获取差集

2.4.2 数据结构

底层结构是dict字典,字典使用哈希表实现的。

Java中的Set的内部实现是用HashMap,只不过所有的value都指向了同一个对象。Redis的set结构也是一样,它的内部结构也是hash,所有的value都指向了同一个内部值。

2.5 哈希

2.5.1 常用操作

操作

说明

hset [map] [key] [value]

赋值

hget [map] [key]

取值

hlen [map]

容量

hexists [map] key]

是否存在

hkeys [map]

所有键值

hincrby [map] [key] [len]

为某个键增加步长

2.5.2 数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。

当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

2.6 有序集合

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

因为元素是有序的,所以可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。

访问有序集合的中间元素也是非常快的,因此够使用有序集合作为一个没有重复成员的智能列表。

2.6.1 常用操作

操作

说明

zadd [zset] [score] [key]

添加元素

zrange [zset] [start] [end]

遍历元素

zange [zset] [start] [end] withscores

遍历元素(展示分值)

zrangebyscore [zset] [start] [end]

按分值排序(从小到大)

zrangebyscore [zset] [start] [end] withscores

按分值排序

zrevrangebyscore [zset] [start] [end ]

按分值倒排(从大到小)

zrem [zset] [key]

移除元素

zincrby [zset] [step] [key]

增加元素分值

zrank [zset] [key]

获取元素排名

zrank [zset] [key] withscores

获取元素排名

2.6.2 数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构:

hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

2.6.3 跳跃表

简介

有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。

对于有序集合的底层实现,可以用数组、平衡树、链表等。

数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。

Redis采用的是跳跃表,跳跃表效率堪比红黑树,实现远比红黑树简单。

实例

对比有序链表和跳跃表,从链表中查询出51:

有序链表:

? 要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

跳跃表:

  • 从第2层开始,1节点比51节点小,向后比较;
  • 21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层;
  • 在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下;
  • 在第0层,51节点为要查找的节点,节点被找到,共查找4次。

从此可以看出跳跃表比有序链表效率要高。

三、配置文件详解

redis.conf

3.1 Units

单位,配置大小单位,开头定义了一些基本的度量单位,只支持?bytes,不支持?bit。大小写不敏感。

比如设置内存大小:

//设置Redis最大占用内存大小为500M maxmemory 500mb

3.2 INCLUDES

包含,多实例的情况可以把公用的配置文件提取出来。

3.3 NETWORK

网络相关配置

参数

格式

说明

bind

bind=127.0.0.1

不写的情况下,无限制接受任何?ip?地址的访问。

如果开启了protected-mode,那么在没有设定?bind ip?且没有设密码的情况下,Redis?只允许接受本机的响应。

protected-mode

protected-mode no

是否开机本机保护模式

port

port 6379

端口号

tcp-backlog

tcp-backlog 511

设置?tcp?的?backlog,backlog?其实是一个连接队列,backlog?队列总和 $=$ 未完成三次握手队列 $+$ 已经完成三次握手队列。

在高并发环境下需要一个高?backlog?值来避免慢客户端连接问题。

timeout

timeout 0

一个空闲的客户端维持多少秒会关闭,0 表示关闭该功能。即永不关闭。

tcp-keepalive

tcp-keepalive 300

对访问客户端的一种心跳检测,每个?n?秒检测一次。

单位为秒,如果设置为 0,则不会进行?Keepalive?检测。

3.4 GENERAL

通用

参数

格式

说明

daemonize

daemonize yes

是否为后台进程

pidfile

pidfile /var/run/redis_6379.pid

记录pid的文件

loglevel

loglevel notice

日志级别

logfile

logfile ""

日志文件

database

databases 16

数据库数量

3.5 SECURITY

访问密码的查看、设置和取消。

在命令中设置密码,只是临时的。重启?redis?服务器,密码就还原了。永久设置,需要在配置文件中进行设置。

3.6 LIMITS

参数

格式

说明

maxclients

maxclients 10000

设置?redis?同时可以与多少个客户端进行连接。

如果达到了此限制,redis?则会拒绝新的连接。

maxmemory

maxmemory 1gb

最大占用内存。

超过这个限制就会开始清除数据、或者对新增数据命令返回失败。

maxmemory-policy

maxmemory-policy noeviction

最大内存限制后数据清除方式:

volatile-lru:使用?LRU?算法移除?key,只对设置了过期时间的键(最近最少使用)。

allkeys-lru:在所有集合?key?中,使用?LRU?算法移除?key。

volatile-random:在过期集合中移除随机的?key,只对设置了过期时间的键。

allkeys-random:在所有集合?key?中,移除随机的?key。

volatile-ttl:移除那些?TTL?值最小的?key,即那些最近要过期的?key。

noeviction:不进行移除。针对写操作,只是返回错误信息

maxmemory-samples

maxmemory-samples 5

设置样本数量,LRU?算法和最小?TTL?算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis?默认会检查这么多个?key?并选择其中?LRU?的那个。

一般设置 3 到 7 的数字,数值越小样本越不准确,但性能消耗越小。

四、发布与订阅

Redis支持的一种消息通讯方式:

订阅端订阅渠道消息,一旦有发布端往渠道中推送消息,订阅端能立即接收到消息。

弊端:

  • 发布的消息不会持久化,即发布的消息仅会推送到当前在线的订阅端。下线或后添加的订阅端不会收到历史发布的消息。

五、新数据类型

5.1 BitMaps

Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。

Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

BitMaps最大占用512M.

即最大的长度可为: 512 * 1024 * 1024 * 8 = 4294967296

操作

操作

说明

setbit key offset value

位赋值

getbit key offset

位取值

bitcount key

计算位1数

bitop [and|or] [destkey] [key1] [key2] [key3]

位操作

示例

127.0.0.1:6379> setbit visit1 1 1
(integer) 0
127.0.0.1:6379> setbit visit1 4 1
(integer) 0
127.0.0.1:6379> setbit visit2 1 1
(integer) 0
127.0.0.1:6379> setbit visit2 10 1
(integer) 0
127.0.0.1:6379> bitcount visit1
(integer) 2
127.0.0.1:6379> bitcount visit2
(integer) 2
127.0.0.1:6379> bitop and visit1&2 visit1 visit2
(integer) 2
127.0.0.1:6379> bitcount visit1&2
(integer) 1
127.0.0.1:6379> bitop or visit1|2 visit1 visit2
(integer) 2
127.0.0.1:6379> bitcount visit1|2
(integer) 3

Bitmaps与set对比

假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表:

set和Bitmaps存储一天活跃用户对比

数据类型

每个用户id占用空间

需要存储的用户量

全部内存量

集合

64位

50000000

64位*50000000 = 400MB

Bitmaps

1位

100000000

1位*100000000 = 12.5MB

很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的。

set和Bitmaps存储独立用户空间对比

数据类型

一天

一月

一年

集合

400M

12GB

144GB

Bitmaps

12.5MB

375MB

4.5GB

但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。

set和Bitmaps存储一天活跃用户对比

数据类型

每个用户id占用空间

需要存储的用户量

全部内存量

集合

64位

100000

64位*100000 = 800KB

Bitmaps

1位

100000000

1位*100000000 = 12.5MB

5.2 HyperLogLog

在工作当中经常会遇到与统计相关的功能需求,比如统计网站 PV(PageView 页面访问量),可以使用 Redis 的 incr、incrby 轻松实现。但像 UV(UniqueVisitor 独立访客)、独立 IP 数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。

解决基数问题有很多种方案:

  • 数据存储在 MySQL 表中,使用 distinct count 计算不重复个数。
  • 使用 Redis 提供的 hash、set、bitmaps 等数据结构来处理。

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。能否能够降低一定的精度来平衡存储空间?Redis 推出了 HyperLogLog。

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8},那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数 (不重复元素) 为 5。 基数估计就是在误差可接受的范围内,快速计算基数。

操作

操作

说明

pfadd [key] [value1] [value2] [value3]

添加基数元素

pfcout [key]

计算基数数量

pfmerge [destkey] [source1] [source2] [source3]

合并基数

示例

127.0.0.1:6379> pfadd skill java mysql
(integer) 1
127.0.0.1:6379> pfadd skill c
(integer) 1
127.0.0.1:6379> pfcount skill
(integer) 3
127.0.0.1:6379> pfadd skill2 chinese english
(integer) 1
127.0.0.1:6379> pfcount skill2
(integer) 2
127.0.0.1:6379> pfmerge allskill skill skill2
OK
127.0.0.1:6379> pfcount allskill
(integer) 6

5.3 Geospatial

Redis 3.2 中增加了对 GEO 类型的支持。GEO,Geographic,地理信息的缩写。

该类型就是元素的 2 维坐标,在地图上就是经纬度。

redis 基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度 Hash 等常见操作。

操作

操作

说明

geoadd [key] [lon1] [lat1] [pos1] [lon2] [lat2] [pos2]

添加经纬度

geopos [key] [pos]

获取地区经纬度

geodist [key] [pos1] [pos2] [unit]

获取两个点的距离

georadius [key] [lon1] [lat1] [radius] [unit]

获取某坐标点范围内的地区

示例:

127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing 114.52 22.52 shenzhen 116.38 39.90 beijing
(integer) 3
127.0.0.1:6379> geopos china:city shanghai beijing
1) 1) "121.47000163793564"
   2) "31.229999039757836"
2) 1) "116.38000041246414"
   2) "39.900000091670925"
127.0.0.1:6379> geodist china:city shanghai beijing km
"1068.1535"
127.0.0.1:6379> geodist china:city beijing chongqing m
"1462950.5002"
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "chongqing"
2) "shenzhen"
127.0.0.1:6379> type china:city
zset

六、Java项目中使用

6.1 Jedis 使用

JedisDemo示例代码

同Jdbc,Java也提供了Jedis这类工具包进行redis连接及操作。

项目中,我们引入Jedis依赖即可使用。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>

示例:

public class JedisDemo1 {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        String ping = jedis.ping();
        System.out.println(ping);
    }

    @Test
    public void demo() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        String s = jedis.flushDB();
        System.out.println(s);
    }

    @Test
    public void demo1() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        Set<String> keys = jedis.keys("*");
        System.out.println(keys);
    }

    @Test
    public void demo2() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        String set = jedis.set("name", "xiang");

        String name = jedis.get("name");
        System.out.println(name);

        jedis.mset("k1", "v1", "k2", "v2");

        List<String> mGet = jedis.mget("k1", "k2");
        System.out.println(mGet);
    }

    @Test
    public void demo3() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        Long lpush = jedis.lpush("list", "v1", "v2", "v3");
        System.out.println(lpush);

        List<String> list = jedis.lrange("list", 0, -1);
        System.out.println(list);
    }

    @Test
    public void demo4() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.sadd("family", "xiang");
        jedis.sadd("family", "yuan");

        Set<String> set = jedis.smembers("family");
        System.out.println(set);
    }


    @Test
    public void demo5() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.hset("desc", "price", "12");
        jedis.hset("desc", "height", "140");

        jedis.hincrBy("desc", "price", 12);
        Map<String, String> desc = jedis.hgetAll("desc");
        System.out.println(desc);
    }

    @Test
    public void demo6() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);

        jedis.zadd("city", 90, "nanjing");
        jedis.zadd("city", 95, "shanghai");
        jedis.zadd("city", 99, "beijing");

        Set<Tuple> city = jedis.zrangeByScoreWithScores("city", 0, 100);
        System.out.println(city);
    }
}

6.2 与SpringBoot整合

整合示例代码

与Springboot整合只需要引入启动器即可

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在application.yml中配置具体redis连接信息:

spring:
  redis:
    port: 6379
    host: 127.0.0.1

示例:

@Slf4j
@RestController
public class MyController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @GetMapping("test")
    public String test(){
        redisTemplate.opsForValue().set("name1","xiang");
        String name1 = redisTemplate.opsForValue().get("name1");
        log.info("name:{}", name1);
        return name1;
    }
}

根据需要自定义redisTemplate:

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        template.setKeySerializer(redisSerializer);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(600))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

七、事务操作

7.1 事务基本操作

Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

但它有区别与数据库的事务,类似的隔离性能够满足,但就无法满足原子性。

Redis的三个命令需要搭配使用:multi、exec、discard

Redis批量提交命令与Mysql事务命令对比

功能

Redis命令

Mysql命令

开启

multi

start transaction

提交

exec

commit

取消

discard

rollback

举例:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2 
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK

redis的事务其实是两段式:

1、命令入队

2、队列命令执行(或取消)

需要注意的是:

若命令在入队时报错,则整队命令都不会执行;

若命令在执行时失败,则该条命令失败,不会影响前面或后面的语句执行。

入队时失败示例:

127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

执行时失败示例:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK

7.2 事务冲突

想想一个场景:有很多人有你的账户,同时去参加双十一抢购。

例子:

  • 一个请求想给金额减 8000;
  • 一个请求想给金额减 5000;
  • 一个请求想给金额减 1000。

最终我们可以发现,总共金额是 10000,如果请求全部执行,那最后的金额变为 - 4000,很明显不合理。

悲观锁

悲观锁 (Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁

乐观锁 (Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set 机制实现事务的。

7.3 乐观锁和事务特性

watch key [key …]

在执行 multi 之前,先执行 watch key1 [key2],可以监视一个 (或多个) key 。

如果在事务执行之前这个 (或这些) key 被其他命令所改动,那么事务将被打断。

示例:

1、预置balance参数

127.0.0.1:6379> set balance 100
OK

2、打开两个客户端同时监视balance值

后执行exec的那个会失败

127.0.0.1:6379> watch balance 
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 110
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
127.0.0.1:6379(TX)> exec
(nil)

unwatch

取消 WATCH 命令对所有 key 的监视。

如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

Redis 事务三特性

单独的隔离操作 :事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

没有隔离级别的概念 :队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。

不保证原子性 :事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 。

7.4 秒杀案例

解决计数器和人员记录的事务操作

秒杀代码流程

public static boolean flashSale(String pid, String uid) {
    if (pid == null || uid == null) {
        System.out.println("用户ID或商品ID不得为空");
        return false;
    }

    try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
        String kcKey = "sk:" + pid + ":kc";
        String usersKey = "sk:" + pid + ":users";
        String kc = jedis.get(kcKey);
        if (kc == null) {
            System.out.println("秒杀活动尚未开始");
            return false;
        }
        if (Integer.parseInt(kc) <= 0) {
            System.out.println("秒杀活动已结束");
            return false;
        }
        if (jedis.sismember(usersKey, uid)) {
            System.out.println("用户不得重复进行秒杀");
            return false;
        }

        jedis.incrBy(kcKey, -1);
        jedis.sadd(usersKey, uid);
        System.out.println(uid + "用户成功秒杀");
        return true;
    }
}

上述代码未考虑并发处理,存在超卖问题

解决超卖问题

利用乐观锁解决超卖问题,监视库存键。

public static boolean flashSale(String pid, String uid) {
    if (pid == null || uid == null) {
        System.out.println("用户ID或商品ID不得为空");
        return false;
    }

    try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
        String kcKey = "sk:" + pid + ":kc";
        String usersKey = "sk:" + pid + ":users";
        String kc = jedis.get(kcKey);
        if (kc == null) {
            System.out.println("秒杀活动尚未开始");
            return false;
        }
        if (Integer.parseInt(kc) <= 0) {
            System.out.println("秒杀活动已结束");
            return false;
        }
        if (jedis.sismember(usersKey, uid)) {
            System.out.println("用户不得重复进行秒杀");
            return false;
        }

        //此处增加监视
        jedis.watch(kcKey);
        Transaction multi = jedis.multi();
        multi.incrBy(kcKey, -1);
        multi.sadd(usersKey, uid);
        List<Object> result = multi.exec();
        if(result == null || result.size() == 0){
            System.out.println(uid + "秒杀失败");
            return false;
        }
        System.out.println(uid + "用户成功秒杀");
        return true;
    }
}

由于乐观锁导致同批次请求处理失败,同时刻的请求会处理失败。虽然请求数量远大于库存数量,但却会存在库存剩余问题。

解决库存剩余问题

上述问题由于乐观锁导致,那考虑使用悲观锁,让请求顺序处理。

Redis需要结合LUA脚本才能保证请求顺序执行。

LUA 脚本在 Redis 中的优势

  • 将复杂的或者多步的 redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数,提升性能。
  • LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作。
  • 但是注意 redis 的 lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用。
  • redis 2.6 版本以后,通过 lua 脚本解决争抢问题,实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

LUA脚本:

local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":kc";
local usersKey="sk:"..prodid..":users";
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then
	return 2;
end
local num = redis.call("get",qtkey);
if tonumber(num)<=0 then 
	return 0;
else
	redis.call("decr",qtkey);
	redis.call("sadd",usersKey,userid);
end;
return 1;

使用LUA脚本处理秒杀:

public static boolean flashSale(String pid, String uid) {
    try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
        String sha1 = jedis.scriptLoad(flushSaleScript);
        Object result = jedis.evalsha(sha1, 2, uid, pid);
        String resultStr = String.valueOf(result);
        if("0".equals(resultStr)){
            System.out.println("已抢空");
            return false;
        }else if("1".equals(resultStr)){
            System.out.println("抢购成功");
            return true;
        }else if("2".equals(resultStr)){
            System.out.println("已经抢购过");
            return false;
        }else{
            System.out.println("抢购异常");
            return false;
        }
    }
}

八、持久化

官网介绍:Redis

Redis 提供了 2 个不同形式的持久化方式:

  • RDB(Redis DataBase)
  • AOF(Append Of File)

8.1 RDB

8.1.1 简介

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是Snapshot快照,它恢复时是将快照文件直接读到内存里。

备份是如何执行的

Redis 会单独创建(fork)一个子进程来进行持久化,首先会将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。

整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。

如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感, RDB 方式要比 AOF 方式更加的高效。

RDB 的缺点是最后一次持久化后的数据可能丢失。

Fork

Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。

在 Linux 程序中,fork () 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux 中引入了 “写时复制技术”。

一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

8.1.2 RDB 持久化流程

问题:在进行bgsave的时,新写入的数据会被保存到RDB中吗?

不会,执行bgsave时,会把父进程内存进行快照一份,然后子进程用自己的快照来遍历写入RDB。

作者:天龙

链接:https://www.zhihu.com/question/449947403/answer/1787213651

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

bgsave 的原理是 fork() + copyonwrite 。

fork()是unix和linux这种操作系统的一个api,而不是Redis的api。

当bgsave执行时,Redis主进程会判断当前是否有fork()出来的子进程,若有则忽略,若没有则会fork()出一个子进程来执行rdb文件持久化的工作,子进程与Redis主进程共享同一份内存空间,所以子进程可以搞他的rdb文件持久化工作,主进程又能继续他的对外提供服务,二者互不影响。

我们说了他们之后的修改内存数据对彼此不可见,但是明明指向的都是同一块内存空间,这是咋搞得?肯定不可能是fork()出来子进程后顺带复制了一份数据出来。

主进程fork()子进程之后,内核把主进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向主进程。这也就是共享了主进程的内存,当其中某个进程写内存时(这里肯定是主进程写,因为子进程只负责rdb文件持久化工作,不参与客户端的请求),CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入内核的一个中断例程。

中断例程中,内核就会把触发的异常的页复制一份(这里仅仅复制异常页,也就是所修改的那个数据页,而不是内存中的全部数据),于是主子进程各自持有独立的一份。

所以就是更改数据的之前进行copy一份更改数据的数据页出来,比如主进程收到了set k 1请求(之前k的值是2),然后这同时又有子进程在rdb持久化,那么主进程就会把k这个key的数据页拷贝一份,并且主进程中k这个指针指向新拷贝出来的数据页地址上,然后进行更改值为1的操作,这个主进程k元素地址引用的新拷贝出来的地址,而子进程引用的内存数据k还是修改之前的。

dump.rdb 文件

在 redis.conf 中配置文件名称,默认为 dump.rdb。

rdb 文件的保存路径,也可以修改。默认为 Redis 启动时命令行所在的目录下 “dir ./”

如何触发 RDB 快照;保持策略

配置文件中默认的快照配置

  • 一小时内有一条记录改动
  • 五分钟内有100条记录改动
  • 60秒内有10000条记录改动
save 3600 1
save 300 100
save 60 10000

命令 save VS bgsave

  • save :save 时只管保存,其它不管,全部阻塞。手动保存,不建议。
  • bgsave:Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
  • 可以通过 lastsave 命令获取最后一次成功执行快照的时间。

flushall 命令

  • 执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义。

优势

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度快

劣势

  • Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑。
  • 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失最后一次快照后的所有修改。

如何停止

动态停止 RDB:redis-cli config set save “”#save 后给空值,表示禁用保存策略。

8.1.3 小总结

8.2 AOF

8.2.1 简介

以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来 (读操作不记录), 只许追加文件但不可以改写文件。

redis启动时会执行AOF每一条写指令恢复数据。

8.2.2 AOF 持久化流程

  1. 客户端的请求写命令会被 append 追加到 AOF 缓冲区内
  2. AOF 缓冲区根据 AOF 持久化策略 [always,everysec,no] 将操作 sync 同步到磁盘的 AOF 文件中
  3. AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量
  4. Redis 服务重启时,会重新加载 AOF 文件中的写操作达到数据恢复的目的

AOF 默认不开启

可以在 redis.conf 中配置文件名称默认为 appendonly.aof 文件中开启,AOF 文件的保存路径,同 RDB 的路径一致。

vi redis.conf
 
//... 
appendonly yes

AOF 和 RDB 同时开启,redis 听谁的?

AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失)。

AOF 启动、修复、恢复

AOF 的备份机制和性能虽然和 RDB 不同,但是备份和恢复的操作同 RDB 一样,都是拷贝备份文件,需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载。

正常恢复

  • 修改默认的 appendonly no,改为 yes。
  • 将有数据的 aof 文件复制一份保存到对应目录 (查看目录:config get dir)。
  • 恢复:重启 redis

异常恢复

  • 修改默认的 appendonly no,改为 yes。
  • 如遇到 AOF 文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof 进行恢复。
  • 备份被写坏的 AOF 文件。
  • 恢复:重启 redis,然后重新加载。

AOF 同步频率设置

  • appendfsync always:始终同步,每次 Redis 的写入都会立刻记入日志;性能较差但数据完整性比较好。
  • appendfsync everysec (默认):每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
  • appendfsync no:redis 不主动进行同步,把同步时机交给操作系统。

8.2.3 Rewrite 压缩

Rewrite 压缩是什么

AOF 采用文件追加方式,文件会越来越大。

为避免出现此种情况,新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。

可以使用命令 bgrewriteaof。

重写原理,如何实现重写

AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写 (也是先写临时文件最后再 rename)。

redis4.0 版本后的重写,是把 rdb 的快照以二进制的形式附在新的 aof 头部,作为已有的历史数据,替换掉原来的流水账操作。(所以aof的实际大小并不会比rbd大很多)

no-appendfsync-on-rewrite:

  • 如果 no-appendfsync-on-rewrite=yes ,不写入 aof 文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
  • 如果 no-appendfsync-on-rewrite=no,还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

触发机制,何时重写

Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发。

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定 Redis 要满足一定条件才会进行重写。

  • auto-aof-rewrite-percentage:设置重写的基准值,文件达到 100% 时开始重写(文件是原来重写后文件的 2 倍时触发)。
  • auto-aof-rewrite-min-size:设置重写的基准值,最小文件 64MB。达到这个值开始重写。
  • 系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为 base_size,
  • 如果 Redis 的 AOF 当前大小 >= base_size +base_size*100% (默认) 且当前大小 >=64mb (默认) 的情况下,Redis 会对 AOF 进行重写。例如:文件达到 70MB 开始重写,降到 50MB,下次100MB开始重写

重写流程

  1. bgrewriteaof 触发重写,判断是否当前有 bgsave 或 bgrewriteaof 在运行,如果有,则等待该命令结束后再继续执行;
  2. 主进程 fork 出子进程执行重写操作,保证主进程不会阻塞;
  3. 子进程遍历 redis 内存中数据到新AOF文件,客户端的写请求同时写入 aof_buf 缓冲区和 aof_rewrite_buf 重写缓冲区,保证原 AOF 文件完整以及新 AOF 文件生成期间的新的数据修改动作不会丢失;
  4. 子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息。主进程把 aof_rewrite_buf 中的数据写入到新的 AOF 文件;
  5. 使用新的 AOF 文件覆盖旧的 AOF 文件,完成 AOF 重写。

bgrewriteaof完后生成的文件为两部分:

  1. 执行bgrewriteaof命令时,redis 内存中的所有数据生成的二进制数据
  2. bgrewriteaof执行期间,客户端的写请求命令,即aof_rewrite_buf中的写命令。
  3. (后续的写命令仍然以命令的方式追加,直至执行新的rewrite操作)

8.2.4 总结

优势

  • 备份机制更稳健,丢失数据概率更低
  • 可读的日志文本,可以处理误操作

劣势

  • 比起 RDB 占用更多的磁盘空间
  • 恢复备份速度要慢
  • 每次读写都同步的话,有一定的性能压力
  • 存在个别 Bug,造成恢复不能

优劣势总结:

8.3 持久化总结 (用哪个好)

官方推荐两个都启用:

  • 如果对数据不敏感,可以选单独用 RDB
  • 不建议单独用 AOF,因为可能会出现 Bug
  • 如果只是做纯内存缓存,可以都不用

官网建议

  • RDB 持久化方式能够快速进行快照存储。
  • AOF 持久化方式记录每次对服务器写的操作,服务器重启时会重新执行这些命令来恢复数据
  • Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大
  • 只做缓存:如果只希望数据在服务器运行的时候存在,可以不使用任何持久化方式。
  • 同时开启两种持久化方式: redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为 AOF 文件保存的数据集要更完整
  • RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只使用 AOF 呢?
    • 建议不要,因为 RDB 更适合用于备份数据库,快速重启,而且不会有 AOF 可能潜在的 bug,留着作为一个万一的手段。
  • 性能建议:
    • 因为 RDB 文件只用作后备用途,建议只在 Slave 上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留 save 900 1 这条规则
    • 如果使用 AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单,只 load 自己的 AOF 文件就可以了
    • AOF代价:一是带来了持续的 IO;二是 AOF rewrite 的最后,将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的
    • 只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M 太小了,可以设到 5G 以上。默认超过原大小 100% 大小时重写可以改到适当的数值。

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-06-26 16:58:01  更:2022-06-26 16:58:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:00:19-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码