Redis学习笔记(一)

学习视频链接: https://www.bilibili.com/video/BV1S54y1R7SB

一、Nosql概述

1.1. 为什么要用Nosql

1、单机MySQL的年代

image.png

90年代,一个基本的网站访问量一般不会太大,单个数据库完全足够!

那个时候,更多的去使用静态网页Html~服务器根本没有太大的压力!

思考一下,这种情况下:整个网站的瓶颈是什么?

1、数据量如果太大、一个机器放不下了!

2、数据的索引(B+Tree),一个机器内存也放不下

3、访问量(读写混合),一个服务器承受不了~

只要你开始出现以上的三种情况之一,那么你就必须要晋级!

2、Memcached(缓存)+MySQL+垂直拆分

网站80%的情况都是在读,每次都要去查询数据库的话就十分的麻烦!所以说我们希望减轻数据的压力,我们可以使用缓存来保证效率!(解决读的效率问题)

发展过程:优化数据结构和索引 ==> 文件缓存(IO) ==> Memcached(当时最热门的技术!)

image.png

3、分库分表 + 水平拆分 + MySQL集群

技术和业务在发展的同时,对人的要求也越来越高!(解决写的问题)

早些年 MyISAM :表锁,十分影响效率!高并发下就会出现严重的锁问题(100W条数据查张三密码,在查询的过程中会将此表锁定,无法查询其余的信息)

转战 Innodb : 行锁(提高效率)

慢慢的就开始使用分库分表来解决写的压力!MySQL在那个年代推出了表分区!这个并没有多少公司使用!

推出 MySQL 集群,很好满足哪个年代的所有需求!

image.png

4、如今最近的年代

2010 – 2020 十年之间,世界已经发生了翻天覆地的变化;(定位,音乐,热榜!)

MySQL 等关系型数据库不够用!数据量很多,变化很快!

MySQL 有些人使用它来存储一些比较大的文件、博客、图片!数据库表很多,效率就低了! 如果有一种数据库来专门处理这种数据,MySQL 压力就变得十分小(研究如何处理这些问题!)大数据的 IO 压力下,表几乎没法更大!(eg:几亿的量,如果添加一个列,即添加几亿的数据)

目前的一个互联网项目(架构)

image.png

为什么要用 NoSQL!

用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!

这时候我们就需要使用NoSQL数据库的,NoSQL可以很好的处理以上的情况

1.2. 什么是NoSQL

NoSQL

NoSQL = Not Only SQL (不仅仅是SQL)

关系型数据库:表格、行、列

泛指非关系型数据库的,随着web2.0互联网的诞生!传统的关系型数据库很难对付web2.0时代!尤其是超大规模的高并发的社区!暴露出来很多难以克服的问题,NoSQL在当今大数据环境下发展的十分迅速, Redis 是发展最快的,而且是我们当下必须要掌握的一个技术!

很多的数据类型用户的个人信息,社交网络,地理位置。这些数据类型的存储不需要一个固定的格式!不需要过多的操作就可以横向扩展的!Map<String,Object> 使用键值对来控制!

NoSQL 特点

解耦!

1、方便扩展(数据之间没有关系,很好扩展!)

2、大数据量高性能(Redis 一秒写 8w次,读取 11w,NoSQL的缓存记录级,是一种细粒度的缓存,性能会比较高!)

3、数据类型是多样型的!(不需要事先设计数据库!随取随用!如果是数据库十分大的表,很多人就无法设计了!)

4、传统 RDBMS(关系型数据库) 和 NoSQL(非关系型数据库)

传统的 RDBMS
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row col
- 操作,数据定义语言
- 严格的一致性
- 基础的事务
- ......
NoSQL
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性,
- CAP定理和BASE(异地多活)初级架构师!(狂神理念:只要学不死,就往死里学!)
- 高性能,高可用,高可扩
- ....

了解: 3V + 3 高

大数据时代的3V:主要是描述问题的

  1. 海量 Volume

  2. 多样 Variety

  3. 实时 Velocity

大数据时代的3高:主要是对程序的要求

  1. 高并发

  2. 高可扩(随时水平拆分,机器不够可以通过扩展机器来调节)

  3. 高性能(保证用户体验和性能!)

真正在公司中的实践: NoSQL+RDBMS一起使用才是最强的,阿里巴巴的架构演进!

技术没有高低之分,就看你如何去使用!(提升内功,思维的提高!)

1.3. 阿里巴巴演进分析

敏捷开发、极限编程

image.png

架构师:没有什么是加一层解决不了的!

# 1、商品的基本信息
		名称、价格、商品信息:
		关系型数据库就可以解决了! MySQL/Oracle(淘宝早年就去IOE)
		淘宝内部的MySQL不是大家用的MySQL,他们根据自身业务对MySQL进行修改
# 2、商品的描述、评论(文字比较多)
		文档型数据库中,MongoDB
# 3、图片
		分布式文件系统 FastDFS
		- 淘宝自己的  TFS
		- Gooale的   GFS
		- Hadoop  	 HJDFS
		- 阿里云的    oss
# 4、商品的关键字(搜索)
		- 搜索引擎 solr elasticsearch
		- ISerach: 多隆(多了解一些技术大佬)
		
# 5、商品热门的波段信息
		- 内存数据库
		- Redis、Tair、Memache
# 6、商品的交易,外部的支付接口
		- 三方应用

一个简单的网页背后的技术一定不是大家所想的那么简单!

大型互联网应用问题:

  • 数据类型太多了
  • 数据员繁多,经常重构!
  • 数据要改造,大面积改造

解决问题:(添加多一层)

image.png

image.png

这里以上都是NoSQL入门概述,不仅能够提高大家的知识,还可以帮助大家了解大厂 的工作内容!

1.4. NoSQL的四大分类

KV键值对:

  • 新浪:Redis

  • 美团:Redis + Tair

  • 阿里、百度: Redis + Memecache

文档型数据库(bson格式和json一样):

  • MongoDB (一般必须要掌握)
    • MongoDB 是一个基于分布式文件存储的数据库,C++ 编写, 主要用来处理大量的文档!
    • MOngoDB 是一个介于关系型数据库和非关系型数据库中的产品!MongoDB 是非关系型数据库中功能最丰富,最像关系型数据库的!
  • ConthDB

列存储数据库

  • HBase
  • 分布式文件系统

图关系数据库

image.png

  • 他不是存图形,放的是关系,比如:朋友圈社交网络,广告推荐!
  • Neo4j,InfoGrid;

四者对比

image.png

二、Redis 入门

2.1. 概述

Redis 是什么?

Redis(Remote Dictionary Server ),即远程字典服务

是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API

image.png

redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

免费和开源!是当下最热门的NoSQL技术之一!也被人们称之为结构化数据库!

Redis 能干吗?

1、内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof

2、效率高,可以用于高速缓存

3、发布订阅系统

4、地图信息分析

5、计时器、计数器(浏览量)

特性

1、多样的数据类型

2、持久化

3、集群

4、事务

学习中需要的东西

1、官网: https://redis.io/

2、中文网: https://www.redis.net.cn/

image.png

3、下载地址:通过官网下载即可

image.png

注意:Window 在 Github 上下载(停更很久!)

Reids推荐都是在Linux服务器上搭建的,我们是基于Linux学习!

2.2. Windows安装

1、下载安装包:

2、下载完毕得到压缩包:

image.png

3、解压到自己电脑上的环境目录下即可!Redis 十分小,只有 5M

image.png

4、开启Redis,双击 redis-server.exe 即可

image.png

5、使用 Redis 客户端来连接 Redis (redis-cli.exe)

image.png

Window 下使用确实简单,但是 Redis 推荐我们使用 Linux 系统使用

image.png

2.3. Linux安装

1、官网下载 Redis

2、将 Redis 安装包解压到 程序/opt 目录下

image.png

3、进入解压后的文件,可以看到我们 Redis 的配置文件

image.png

4、基本的环境安装

# 安装环境
yum install gcc-c++
# 配置
make
# 安装
make install

5、redis 的默认安装路径:/usr/local/bin

image.png

6、将 redis 配置文件。复制到我们当前目录下

image.png

7、redis 默认不是后台启动的,修改配置文件!

image.png

8、启动 Redis 服务!

image.png

9、使用 redis-cli 进行连接测试!

image.png

10、查看redis的进程是否开启

image.png

11、如何关闭 Redis 服务? shutdown

image.png

12、再次查看进程是否存在

image.png

13、后面我们多会使用单机多 Redis 启动集群测试!

2.4. 测试性能

redis-benchmark 是一个压力测试工具!

官方自带的性能测试工具

redis-benchmark 命令参数:

image.png

简单测试:

# 测试: 100个并发连接 100000请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000

image.png

如何查看这些分析?

image.png

2.5. 基础知识

image.png

默认使用的是第0个

可以使用 select 来切换数据库

127.0.0.1:6379> select 3 # 切换数据库
OK
127.0.0.1:6379[3]> DBSIZE # 查看DB大小!
(integer)0

image.png

127.0.0.1:6379[3] keys * # 查看数据库所有的key
1)"name"

清除当前数据库 flushdb

清除全部数据库的内容 flushAll

127.0.0.1:6379[3]> flushdb 
OK
127.0.0.1:6379[3]>keys *
(empty list or set)

Redis 是单线程的!

明白 Redis 是很快的,官方表示,Redis 是基于内存操作,CPU不是 Redis 性能瓶颈,Redis 的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!

Redis 是C语言写的,官方提供的数据为100000+的 QPS,完全不比同样是使用 key-value 的 Memecache 差!

Redis 为什么单线程还这么快?

1、误区1:高性能的服务器一定是多线程的?

2、误区2:多线程(CPU上下文会切换!)一定比单线程效率高!

要对 CPU > 内存 > 硬盘 的速度要有所了解!

核心:redis 是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!

三、五大数据类型

官网文档

image.png

全段翻译:

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库缓存消息中间件MQ。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

3.1. Redis-key

127.0.0.1:6379> keys * # 查看所有的key
(empty list or set)
127.0.0.1:6379> set name kuangshen # set key
OK
127.0.0.1:6379> keys *
1)"name"
127.0.0.1:6379> set age 1
oK
127.0.0.1:6379> keys *
1)"age"
2)"name"
127.0.0.1:6379> EXISTS name # 判断当前的key是否存在
(integer)1
127.0.0.1:6379> EXISTS name1
(integer)0
127.0.0.1:6379> move name 1 # 移除当前的key
(integer)1
127.0.0.1:6379> keys *
1)"age"
127.0.0.1:6379> set name qinjiang
OK
127.0.0.1:6379> keys *
1)"age"
2)"name"
127.0.0.1:6379> get name
"qinjiang"
127.0.0.1:6379> EXPIRE name 10 # 设置key的过期时间,单位是秒
(integer)1
127.0.0.1:6379> tt1 name # 查看当前key的剩余时间
(integer)4
127.0.0.1:6379> tt1 name
(integer)3
127.0.0.1:6379> tt1 name
(integer)2
127.0.0.1:6379> ttl name
(integer)1
127.0.0.1:6379> ttl name
(integer)-2
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> type age # 查看当前key的类型
string

# 判断是否存在
exists key
# 移除某 key
move key
# 计时器倒计时移除某key 
expire key second
# 查看当前key的剩余时间
ttl key
# 查看当前key的类型
type key
# 
append

strlen

后面如果遇到不会或者不认识的命令,可以直接在官网查看帮助文档

image.png

3.2. String(字符串)

###############################################################
127.0.0.1:6379> get key1 # 获得值
"VI"
127.0.0.1:6379> keys * # 获得所有的key
1)"key1"
127.0.0.1:6379> EXISTS key1 # 判断某一个key是否存在
(integer)1
127.0.0.1:6379> APPEND key1 "he11o" # 追加字符串,如果当前key不存在,就相当于setkey
(integer)7
127.0.0.1:6379> get key1
"v1he11o"
127.0.0.1:6379> STRLEN key1 # 获取字符串的长度!
(integer)7
127.0.0.1:6379> APPEND key1 ",kaungshen"
(integer)17
127.0.0.1:6379> strlen key1
(integer)17
127.0.0.1:6379> get key1
"v1he11o,kaungshen"

###############################################################
# i++  incr key 相当于i++   decr key 相当于i--
# 步长 i+=  incrby key length   incrby key length 相当于i+=length
127.0.0.1:6379> set views 0  # 初始浏览量为0
OK
127.0.0.1:6379> get views
"O"
127.0.0.1:6379> incr views  # 自增1 浏览量变为1
(integer)1
127.0.0.1:6379> incr views
(integer)2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views  # 自减1 浏览量变为0
(integer)1
127.0.0.1:6379> decr views
(integer)0
127.0.0.1:6379> decr views
(integer)-1
127.0.0.1:6379> get views
"-1"
127.0.0.1:6379> INCRBY views 10 # 可以设置步长,指定增量
(integer) 9
127.0.0.1:6379> INCRBY views 10
(integer) 19
127.0.0.1:6379> DECRBY views 5
(integer) 14

###############################################################
# 字符串范围 range
127.0.0.1:6379> set key1 "he1lo,kuangshen" # 设置key1的值
oK
127.0.0.1:6379> get key1
"he11o,kuangshen"
127.0.0.1:6379> GETRANGE key1 0 3 # 截取字符串[0,3]
"he11"
127.0.0.1:6379> GETRANGE key10 -1 # 获取全部的字符串和get key是一样的
"he11o,kuangshen"

# 替换 
127.0.0.1:6379> set key2 abcdefg 
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> SETRANGE key2 1 xx # 替换指定位置开始的字符串
(integer)7
127.0.0.1:6379> get key2
"axxdefg"

##############################################################
# setex (set with expire) # 设置过期时间
# setnx (set if not exist) # 不存在再设置(在分布式锁中会常常使用)

127.0.0.1:6379> setex key3 30 "hello" # 设置key3的值为he11o,30秒后过期
0K
127.0.0.1:6379> ttl key3
(integer)26
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx mykey "redis" # 如果mykey不存在,创建mykey
(integer)1
127.0.0.1:6379> keys *
1)"key2"
2)"mykey"
3)"keyl"
127.0.0.1:6379>ttl key3
(integer)-2
127.0.0.1:6379>setnx mykey "MongoDB" # 如果mykey存在,创建失败!
(integer)0
127.0.0.1:6379> get mykey
"redis"

###############################################################
# 批量设置或获取 mset mget 
127.0.0.1:6379> mset kl v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> keys *
1)"k1"
2)"k2"
3)"k3"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1)"v1"
2)"v2"
3)"v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # msetnx是一个原子性的操作,要么一起成功,要么一起失败!
(integer)0
127.0.0.1:6379> get k4
(nil)

# 对象
set user:1{name:zhangsan,age:3} # 设置一个user:1对象值为json字符来保存一个对象!
# 这里的key是一个巧妙的设计:user:{id}:{filed},如此设计在Redis中是完全ok了!
127.0.0.1:6379>mset user:1:name zhangsan user:1:age 2
oK
127.0.0.1:6379>mget user:1:name user:1:age
1)"zhangsan"
2)"2"

#############################################################
getset # 先get然后在set

127.0.0.1:6379> getset db redis # 如果不存在值,则返回nil
(nil)
127.0.0.1:6379> get db
"redis
127.0.0.1:6379> getset db mongodb # 如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"

String类似的使用场景:value除了是我们的字符串还可以是我们的数字!

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象缓存存储

3.3. List(列表)

基本的数据类型,列表

image.png

在 Redis 中,我们可以把 list 玩成 栈、队列、阻塞队列!

所有的 list 命令都是 l 开头的

##############################################################
127.0.0.1:6379> LPUSH list one # 将一个值或者多个值,插入到列表头部(左)
(integer)1
127.0.0.1:6379> LPUSH list two
(integer)2
127.0.0.1:6379> LPUSH list three
(integer)3
127.0.0.1:6379> LRANGE list 0 -1 # 获取list中的值
1)"three"
2)"two"
3)"one"
127.0.0.1:6379> LRANGE list 0 1 # 通过去间获取具体的值
1)"three"
2)"two"
127.0.0.1:6379> Rpush list right #将一个值或者多个值,插入到列表位部(右)
(integer)4
127.0.0.1:6379> LRANGE list 0 -1
1)"three"
2)"two"
3)"one"
4)"right"

###############################################################
LPOP 左边移除
RPOP 右边移除

127.0.0.1:6379> LRANGE list 0 -1
1)"three"
2)"two"
3)"one"
4)"righr"
127.0.0.1:6379> Lpop 1ist # 移除1ist的第一个元素
"three"
127.0.0.1:6379> Rpop list # 移除1ist的最后一个元素
"righr"
127.0.0.1:6379>LRANGE list 0 -1
1)"two"
2)"one"

###############################################################
LIndex

127.0.0.1:6379>LRANGE list 0 -1
1)"two"
2)"one"
127.0.0.1:6379>1index list 1 # 通过下标获得list中的某一个值!
"one"
127.0.0.1:6379>lindex list 0
"two"
###############################################################
Llen

127.0.0.1:6379> Lpush list one
(integer)1
127.0.0.1:6379> Lpush list two
(integer)2
127.0.0.1:6379> Lpush list three
(integer)3
127.0.0.1:6379> Llen 1ist # 返回列表的长度
(integer)3
##############################################################
Lrem  移除指定的id

127.0.0.1:6379> LRANGE list 0 -1
1)"three"
2)"three"
3)"two"
4)"one"
127.0.0.1:6379> Lrem list 1 one # 移除list集合中指定个数的value,精确匹配
(integer)1
127.0.0.1:6379> LRANGE list 0 -1
1)"three"
2)"three"
3)"two"
127.0.0.1:6379> Lrem list 1 three
(integer)1
127.0.0.1:6379> LRANGE list 0 -1
1)"three"
2)"two"
127.0.0.1:6379> Lpush list three
(integer)3
127.0.0.1:6379> Lrem list 2 three
(integer)2
127.0.0.1:6379> LRANGE list 0 -1
1)"two"

##############################################################
Ltrim 修剪 

127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> Rpush mylist "hello"
(integer)1
127.0.0.1:6379> Rpush mylist "hello1"
(integer)2
127.0.0.1:6379> Rpush mylist "hello2"
(integer)3
127.0.0.1:6379> Rpush mylist "hello3"
(integer)4
127.0.0.1:6379> Ltrim mylist 1 2 # 通过下标截取指定的长度,这个1ist已经被改变了,截断了只剩下截取的元素!
127.0.0.1:6379> LRANGE mylist 0 -1
1)"hello1"
2)"hello2"

##############################################################
rpoplpush # 移除列表的最后一个元素,将他移动到新的列表中!

127.0.0.1:6379> rpush mylist "hel1o"
(integer)1
127.0.0.1:6379> rpush mylist "hello1"
(integer)2
127.0.0.1:6379> rpush mylist "hello2"
(integer)3
127.0.0.1:6379> rpoplpush mylist myotherlist #移除列表的最后一个元素,将他移动到新的列表中!
"he11o2"
127.0.0.1:6379> Lrange mylist 0 -1 # 查看原本的列表
1)"hello"
2)"hello1"
127.0.0.1:6379> Lrange myotherlist 0 -1 #查看目标列表中,确实存在改值!
1)"hello2"

##############################################################
Lset # 将列表中指定下标的值替换为另外一个值,更新操作

127.0.0.1:6379> EXISTS 1ist # 判断这个列表是否存在
(integer)0
127.0.0.1:6379> Lset list 0 item # 如果不存在列表我们去更新就会报错
(error)ERR no such key
127.0.0.1:6379> Lpush list value1
(integer)1
127.0.0.1:6379> LRANGE list 0 0
1)"value1"
127.0.0.1:6379> 1set list 0 item # 如果存在,更新当前下标的值
OK
127.0.0.1:6379> LRANGE list 0 0
1)"item"
127.0.0.1:6379> Lset list 1 other # 如果不存在,则会报错!|
(error)ERR index out of range

##############################################################
Linsert # 将某个具体的value插入到列表中某个元素的前面或者后面!

127.0.0.1:6379> Rpush mylist "hello"
(integer)1
127.0.0.1:6379> Rpush mylist "world"
(integer)2
127.0.0.1:6379> LINSERT mylist before "world" "other"
(integer)3
127.0.0.1:6379> LRANGE mylist0-1
1)"hello"
2)"other"
3)"world"
127.0.0.1:6379> LINSERT mylist after world new
(integer)4
127.0.0.1:6379> LRANGE mylist 0 -1
1)"hello"
2)"other"
3)"world"
4)"new"

小结

  • 他实际上是一个链表,before Node after,left,right都可以插入值
  • 如果key不存在,创建新的链表
  • 如果key存在,新增内容
  • 如果移除了所有值,空链表,也代表不存在!
  • 在两边插入或者改动值,效率最高!中间元素,相对来说效率会低一点

使用场景

  • 消息排队
  • 消息队列(Lpush,Rpop)
  • 栈(Lpush,Lpop)

3.4. Set(集合)

set中的值是不能重复的!

所有的 set 命令开头都是 s

##############################################################
sadd 添加元素  、 smembers 查看指定set的所有值

127.0.0.1:6379> sadd myset "he1lo" # set集合中添加元素
(integer)1
127.0.0.1:6379> sadd myset "kuangshen"
(integer)1
127.0.0.1:6379> sadd myset "lovekuangshen"
(integer)1
127.0.0.1:6379> SMEMBERS myset # 查看指定set的所有值
1)"hello"
2)"1ovekuangshen"
3)"kuangshen"
127.0.0.1:6379> SISMEMBER myset he1lo # 判断某一个值是不是在set集合中!
(integer)1
127.0.0.1:6379> SIsMember myset world
(integer)0

##############################################################
scard 获取set集合中的元素个数

127.0.0.1:6379> scard myset # 获取set集合中的内容元素个数!
(integer)4

##############################################################
srem 移除 set 集合中的指定元素 

127.0.0.1:6379> srem myset hello # 移除set集合中的指定元素
(integer)1
127.0.0.1:6379> scard myset
(integer)3
127.0.0.1:6379> SMEMBERS myset
1)"lovekuangshen2"
2)"1ovekuangshen'"
3)"kuangshen"

##############################################################
set 无序不重复集合,抽随机 SRandMember

127.0.0.1:6379> SMEMBERS myset
1)"lovekuangshen2"
2)"1ovekuangshen"
3)"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset # 随机抽选出一个元素
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset
"kuangshen"
127.0.0.1:6379> SRANDMEMBER myset 2 # 随机抽选出指定个数的元素
1)"lovekuangshen"
2)"1ovekuangshen2"
127.0.0.1:6379> SRANDMEMBER myset 2
1)"lovekuangshen"
2)"lovekuangshen2"
127.0.0.1:6379> SRANDMEMBER myset # 随机抽选出一个元素
"1ovekuangshen2"

##############################################################
删除指定的key,随机删除

127.0.0.1:6379> SMEMBERS myset
1)"lovekuangshen2"
2)"lovekuangshen"
3)"kuangshen"
127.0.0.1:6379> spop myset # 随机删除一些set集合中的元素
"1ovekuangshen2"
127.0.0.1:6379> spop myset
"lovekuangshen"
127.0.0.1:6379> SMEMBERS myset
1)"kuangshen"

##############################################################
将一个指定的值,移动到另一个set集合中
127.0.0.1:6379> sadd myset "hello"
(integer)1
127.0.0.1:6379> sadd myset "world"
(integer)1
127.0.0.1:6379> sadd myset "kuangshen"
(integer)1
127.0.0.1:6379> sadd myset2 "set2"
(integer)1
127.0.0.1:6379> smove myset myset2 "kuangshen"  # 将一个指定的值,移动到另外一个set集合!
(integer)1
127.0.0.1:6379> smembers myset
1)"world"
2)"hello"
127.0.0.1:6379> smembers myset2
1)"kuangshen"
2)"set2"

##############################################################
微博,B站,共同关注!(并集)
数字集合类:
 - 差集 SDIFF
 - 交集 SINTER
 - 并集 SUNION

127.0.0.1:6379> SDIFF keyl key2 # 差集
1)"b"
2)"a"
127.0.0.1:6379> SINTER key1 key2 # 交集共同好友就可以这样实现
1)"c"
127.0.0.1:6379> SUNION key1 key2 # 并集
1)"b"
2)"c"
3)"e"
4)"a"
5)"d"

使用场景

  • 微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!
  • 共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)

3.5. Hash(哈希)

Map 集合 , key - map!时候这个值是一个map集合!本质和 String 类型没有太大区别,还是一个简单的 key-value!

Redis 中 Hash 的语法是前面多加 h

###############################################################
127.0.0.1:6379> hset myhash field1 kuangshen # set一个具体key-vlaue
(integer)1
127.0.0.1:6379> hget myhash field1 # 获取一个字段值
"kuangshen"
127.0.0.1:6379> hmset myhash fieldl hello field2 world # set多个key-vlaue
127.0.0.1:6379> hmget myhash fieldl field2 # 获取多个字段值
1)"hello"
2)"world"
127.0.0.1:6379> hgetall myhash # 获取全部的数据,
1)"field1"
2)"hello"
3)"field2"
4)"world"
127.0.0.1:6379> hdel myhash field1 # 删除hash指定key字段!对应的value值也就消失了!
(integer)1
127.0.0.1:6379> hgetall myhash
1)"field2"
2)"world"
###############################################################
hlen 获取hash表的长度

127.0.0.1:6379> hmset myhash field1 hello field2 world
OK
127.0.0.1:6379> HGETALL myhash
1)"field2"
2)"world"
3)"field1"
4)"hello"
127.0.0.1:6379> hlen myhash # 获取hash表的字段数量!
(integer)2

###############################################################
hexists  判断hash中指定字段是否存在
hsetnx 判断hash中指定字段是否存在,如果存在则不做操作;如果不存在则添加此字段

127.0.0.1:6379> HEXISTS myhash field1 # 判断hash中指定字段是否存在!
(integer)1
127.0.0.1:6379> HEXISTS myhash field3
(integer)0
127.0.0.1:6379> hsetnx myhash field4 hello # 如果不存在则设置
(integer)1
127.0.0.1:6379> hsetnx myhash field4 world # 如果存在则不设置
(integer)0

###############################################################
#只获得所有field
#只获得所有value
127.0.0.1:6379> hkeys myhash # 只获得所有field
1)"field2"
2)"field1"
127.0.0.1:6379> hvals myhash # 只获得所有value
1)"world"
2)"hello"

###############################################################
incr decr

127.0.0.1:6379> hset myhash field3 5 # 指定增量
(integer)1
127.0.0.1:6379> HINCRBY myhash field3 1 
(integer)6
127.0.0.1:6379> HINCRBY myhash field3 -1
(integer)5

###############################################################
127.0.0.1:6379> hset user name zhangsan
(integer) 1
127.0.0.1:6379> hset user age 20
(integer) 1
127.0.0.1:6379> hset user height 190
(integer) 1
127.0.0.1:6379> hgetall user
1) "name"
2) "zhangsan"
3) "age"
4) "20"
5) "height"
6) "190"

###############################################################

使用场景

  • hash变更的数据 user name age,尤其是是用户信息之类的,经常变动的信息!hash更适合于对象的存储,String更加适合字符串存储!

3.6. Zset(有序集合)

在set的基础上,增加了一个值,set k1 v1 有序集合:zset k1 score1 v1

在 Redis 中 Zset 数据类型语法的开头为 z

127.0.0.1:6379>zadd myset 1 one # 添加一个值
(integer)1
127.0.0.1:6379>zadd myset 2 two 3 three # 添加多个值
(integer)2
127.0.0.1:6379>ZRANGE myset 0 -1
1)"one"
2)"twol"
3)"three"
################################################################
排序如何实现 
zRangeByScoure key min max  # 正序
zRevrange key 0 -1  # 倒序


127.0.0.1:6379> zadd salary 2500 xiaohong # 添加三个用户
(integer)1
127.0.0.1:6379> zadd salary 5000 zhangsan
(integer)1
127.0.0.1:6379> zadd salary 500 kaungshen
(integer)1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf # 显示全部的用户从小到大!
1)"kaungshen"
2)"xiaohong"
3)"zhangsan"
127.0.0.1:6379> zRevrange salary 0 -1 # 从大到小进行排序
1)"zhangsan"
2)"xiaohong"
3)"kaungshen"
127.0.0.1:6379>ZRANGEBYSCORE salary -inf +inf withscores # 显示全部的用户并且附带成绩
1)"kaungshen"
2)"500"
3)"xiaohong"
4)"2500"
5)"zhangsan"
6)"5000"
127.0.0.1:6379>ZRANGEBYSCORE salary-inf 2500 withscores # 显示工资小于2500员工的升序排序!
1)"kaungshen"
2)"500"
3)"xiaohong"
4)"2500"

##############################################################
zrem 移除元素 

127.0.0.1:6379> zrange salary 0 -1
1)"kaungshen"
2)"xiaohong"
3)"zhangsan"
127.0.0.1:6379> zrem salary xiaohong # 移除有序集合中的元素
(integer)1
127.0.0.1:6379> zrange salary 0 -1
1)"kaungshen"
2)"zhangsan"
127.0.0.1:6379> zcard salary # 获取有序集合中的个数
(integer)2

##############################################################
获取指定区间的成员数量

127.0.0.1:6379> zadd myset 1 hello
(integer)1
127.0.0.1:6379> zadd myset 2 world 3 kuangshen
(integer)2
127.0.0.1:6379> zcount myset 1 3 # 获取指定区间的成员数量!
(integer)3
127.0.0.1:6379> zcount myset 1 2
(integer)2

其余的一些 API 可以从官网中查看文档进行学习

使用场景

  • set 排序 存储班级成绩表,工资表排序
  • 普通消息 1 ,重要消息 2 ,带权重进行判断!
  • 排行榜应用实现, 取 Top N 测试

四、三种特殊数据类型

4.1. geospatial 地理位置

朋友的定位,附近的人,打车距离计算?

Redis 的 Geo 在 Redis3.2 版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人!

可以查询一些测试数据: http://www.jsons.cn/lngcode/

只有六个命令

image.png

官方文档: https://www.redis.net.cn/order/3685.html

geoadd

# geoadd 添加地理位置
# 规则:两级无法直接添加,我们一般会下载城市数据,直接通过Java程序一次性导入
# 有效的经度从-180度到180度。
# 有效的纬度从-85.05112878度到85.05112878度。
# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。
# 127.0.0.1:6379>geoadd china:city 39.90116.40 beijin
#(error)ERR invalid longitude,latitude pair 39.900000,116.400000
# 参数 key 值(纬度、经度、名称)
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer)1
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 chongqi 114.05 22.52 shenzhen
(integer)2
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xianmen
(integer)2
# 

geopos

获取当前定位:一定是一个坐标值!

127.0.0.1:6379> GEOPOS china:city beijing #获取指定的城市的经度和纬度!
1)1)"116.39999896287918091"
2)"39.90000009167092543"
127.0.0.1:6379> GEoPoS china:city beijing chongqi
1)1)"116.39999896287918091"
2)"39.90000009167092543"
2)1)"106.49999767541885376"
2)"29.52999957900659211"

geodist

两人之间的距离!

单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。
 127.0.0.1:6379> GEoDIST china:city beijing shanghai km # 查看上海到北京的直线距离
"1067.3788"
127.0.0.1:6379> GEoDIST china:city beijing chongqi km # 查看重庆到北京的直线距离
"1464.0708"

georadius 以给定的经纬度为重心,找出某一半径内的元素

我附近的人?(获取所有附近的人的地址,定位!)通过半径来查询

获得指定数量的人,200

所有数据应该都录入:china:city,才会让结果更加准确!

127.0.0.1:6379> GEORADIUS china: city 110 301000 km # 以 110,30 这个经纬度为中心,寻找城市1000km内的城市
1)"chongqi"
2)"xian"
3)"shengzhen"
4)"hangzhou"
127.0.0.1:6379> GEORADIUS china: city 110 30 500 km
1)"chongqi"
2)"xian"
127.0.0.1:6379> GEORADIUS china: city 110 30 500 km withdist # 显示到中间距离的位置
1)1)"chongqi"
  2)"341.9374"
2)1)"xian"
  2)"483.8340"
127.0.0.1:6379> GEORADIUS china: city 110 30 500 km withcoord # 显示他人的定位信息
1)1)"chongqi"
  2)1)"106.49999767541885376"
    2)"29.52999957900659211"
2)1)"xian"
  2)1)"108.96000176668167114"
    2)"34.25999964418929977"
127.0.0.1:6379> GEORADIUS china: city 110 30 500 km withdist withcoord count 1  # 筛选出指定的结果!
1)1)"chongqi"
  2)"341.9374"
  3)1)"106.49999767541885376"
    2)"29.52999957900659211"
127.0.0.1:6379> GEORADIUS china: city 110 30 500 km withdist withcoord count 2
1)1)"chongqi"
  2)"341.9374"
  3)1)"106.49999767541885376"
    2)"29.52999957900659211"
2)1)"xian"
  2)"483.8340"
  3)1)"108.96000176668167114"
    2)"34.25999964418929977"

geoRadiusByMember

# 找出位于指定元素周围的其他元素
127.0.0.1:6379> GEORADIUSBYMEMBER china: city beijing 1000 km
1)"beijing"
2)"xian"
127.0.0.1:6379> GEORADIUSBYMEMBER china: city shanghai 400 km
1)"hangzhou"
2)"shanghai"

geoHash 返回一个或多个位置元素的 Geohash 表示

该命令将返回11个字符的Geohash字符串!

# 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近!
127.0.0.1:6379>geohash china: city beijing chongqi
1)"wx4fbxxfkeo"
2)"wm5xzrybtyo"

GEO底层的实现原理其实就是Zset!我们可以使用Zset命令来操作geo!

127.0.0.1:6379> ZRANGE china: city 0 -1 # 查看地图中全部的元素
1)"chongqi"
2)"xian"
3)"shengzhen"
4)"hangzhou"
5)"shanghai"
6)"beijing"
127.0.0.1:6379> zrem china: city beijing # 移除指定元素
(integer)1
127.0.0.1:6379> ZRANGE china: city 0 -1
1)"chongqi"
2)"xian"
3)"shengzhen"
4)"hangzhou"
5)"shanghai"

4.2. Hyperloglog

什么是基数?

A{1,3,5,7,8,7}
B(1,3,5,7,8}
基数(不重复的元素)=5,可以接受误差!

简介

Redis 2.8.9版本就更新了Hyperloglog数据结构!

Redis Hyperloglog 基数统计的算法!

网页的UV(一个人访问一个网站多次,但是还是算作一个人!)

  • 传统的方式,set 保存用户的id,然后就可以统计 set 中的元素数量作为标准判断!(因为 set 中的元素不会出现重复,所以得出的访问量是准确的)
    • 缺点:这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id;
  • Hyperloglog
    • 优点:占用的内存是固定,2^64 不同的元素的基数,只需要使用12KB内存!如果要从内存角度来比较,Hyperloglog 首选!

测试使用

Redis 的 Hyperloglog:

  • 添加元素( pfadd key element [element...]
  • 统计某元素的基数数量 ( pfcount key [key...]
  • 合并两组元素取两组元素的并集( pfMerge destkey sourcekey [sourcekey...]
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 # 统计 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

如果允许容错,那么一定可以使用 Hyperloglog !

如果不允许容错,就使用 set 或者自己的数据类型即可!

4.3. Bitmaps

位存储

统计疫情感染人数:0 1 0 1

统计用户信息,活跃,不活跃!登录、未登录!打卡,365天打卡!两个状态的业务都可以使用 Bitmaps!

Bitmaps 位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态!
365天=365bit 1字节=8bit 46个字节左右!

测试

Redis 的 Bitmaps:

  • 添加元素和状态( setbit key offset value
  • 获取元素某下标的状态 ( getbit key offset
  • 统计某元素状态为 1 的数量 ( bitcount key [start end]

image.png

使用 bitmap 来记录 周一到周日的打卡!

周一:1周二:0周三:0周四:1……

image.png

查看某一天是否有打卡!

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

五、事务

Redis 事务本质:一组命令的集合!一个事务中的所有命令都会被序列化,在事务执行过程的中,会按照顺序执行!

一次性、顺序性、排他性!执行一些列的命令!

Redis事务没有没有隔离级别的概念!

所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!exec

Redis单条命令式保存原子性的,但是事务不保证原子性!

Redis 的事务:

  • 开启事务( multi
  • 命令入队( ...
  • 执行事务( exec

正常制行事务!

127.0.0.1:6379> multi # 开启事务
OK
# 命令入队
127.0.0.1:6379> set kl 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> multi # 开启事务
OK
127.0.0.1:6379> set kl 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> multi
OK
127.0.0.1:6379> set kl 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)

运行时异常( 1 / 0 ),如果事务队列中存在语法性问题,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常!

127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1 # 能入队,但执行的时候会报错!(字符串无法+1)
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"

监控!Watch(面试常问!)

悲观锁:

  • ·悲观,认为什么时候都会出问题,无论做什么都会加锁!

乐观锁:

  • 很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据(如果被修改了数据,更新数据失败)
  • 获取 version
  • 更新的时候比较 version

Redis 监控测试

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功!
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1)(integer)80
2)(integer)20

测试多线程(事务启动后另一边修改值),使用 watch 可以当做 Redis 乐观锁操作!

127.0.0.1:6379> watch money # 监视 Money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
127.0.0.1:6379> exec # 执行之前另外一个线程修改了我们的值,这个时候,就会导致事务执行失败!
(nil)

如果修改失败,重新获取值即可!

image.png

乐观锁实现业务:

  • 秒杀业务系统

六、Jedis

我们要使用 Java 来操作 Redis ,那么可以使用 Jedis

什么是 Jedis?是 Redis 官方推荐的 Java 连接开发工具! 使用 Java 操作 Redis 中间件!如果你要使用 Java 操作 Redis,那么一定要对 Jedis 十分熟悉!

其他语言:官网中有推荐不同语言对应的Redis操作包

1、导入对应的依赖

<dependencies>
  <!-- jedis -->
  <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
  </dependency>
	<!-- fastjson -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
  </dependency>
</dependencies>

2、编码测试:

  • 连接数据库
  • 操作命令
  • 断开连接
import redis.clients.jedis.Jedis;

public class TestPing {
  public static void main(String[] args) {
    // 1、new Jedis 对象即可
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    // Jedis 所有的命令就是我们之前学习的所有指令!
    System.out.println(jedis.ping());
  }
}

输出

image.png

6.1. 常用的API

对 String 的操作命令

public class TestString {
  public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.flushDB();
    System.out.println("===========增加数据===========");
    System.out.println(jedis.set("key1","value1"));
    System.out.println(jedis.set("key2","value2"));
    System.out.println(jedis.set("key3", "value3"));
    System.out.println("删除键key2:"+jedis.del("key2"));
    System.out.println("获取键key2:"+jedis.get("key2"));
    System.out.println("修改key1:"+jedis.set("key1", "value1Changed"));
    System.out.println("获取key1的值:"+jedis.get("key1"));
    System.out.println("在key3后面加入值:"+jedis.append("key3", "End"));
    System.out.println("key3的值:"+jedis.get("key3"));
    System.out.println("增加多个键值对:"+jedis.mset("key01","value01","key02","value02","key03","value03"));
    System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));
    System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03","key04"));
    System.out.println("删除多个键值对:"+jedis.del("key01","key02"));
    System.out.println("获取多个键值对:"+jedis.mget("key01","key02","key03"));

    jedis.flushDB();
    System.out.println("===========新增键值对防止覆盖原先值==============");
    System.out.println(jedis.setnx("key1", "value1"));
    System.out.println(jedis.setnx("key2", "value2"));
    System.out.println(jedis.setnx("key2", "value2-new"));
    System.out.println(jedis.get("key1"));
    System.out.println(jedis.get("key2"));

    System.out.println("===========新增键值对并设置有效时间=============");
    System.out.println(jedis.setex("key3", 2, "value3"));
    System.out.println(jedis.get("key3"));
    try {
      TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(jedis.get("key3"));

    System.out.println("===========获取原值,更新为新值==========");
    System.out.println(jedis.getSet("key2", "key2GetSet"));
    System.out.println(jedis.get("key2"));

    System.out.println("获得key2的值的字串:"+jedis.getrange("key2", 2, 4));
  }
}

对 List 的操作命令

public class TestList {
  public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.flushDB();
    System.out.println("===========添加一个list===========");
    jedis.lpush("collections", "ArrayList", "Vector", "Stack", "HashMap", "WeakHashMap", "LinkedHashMap");
    jedis.lpush("collections", "HashSet");
    jedis.lpush("collections", "TreeSet");
    jedis.lpush("collections", "TreeMap");
    System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));//-1代表倒数第一个元素,-2代表倒数第二个元素,end为-1表示查询全部
    System.out.println("collections区间0-3的元素:" + jedis.lrange("collections", 0, 3));
    System.out.println("===============================");
    // 删除列表指定的值 ,第二个参数为删除的个数(有重复时),后add进去的值先被删,类似于出栈
    System.out.println("删除指定元素个数:" + jedis.lrem("collections", 2, "HashMap"));
    System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
    System.out.println("删除下表0-3区间之外的元素:" + jedis.ltrim("collections", 0, 3));
    System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
    System.out.println("collections列表出栈(左端):" + jedis.lpop("collections"));
    System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
    System.out.println("collections添加元素,从列表右端,与lpush相对应:" + jedis.rpush("collections", "EnumMap"));
    System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
    System.out.println("collections列表出栈(右端):" + jedis.rpop("collections"));
    System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
    System.out.println("修改collections指定下标1的内容:" + jedis.lset("collections", 1, "LinkedArrayList"));
    System.out.println("collections的内容:" + jedis.lrange("collections", 0, -1));
    System.out.println("===============================");
    System.out.println("collections的长度:" + jedis.llen("collections"));
    System.out.println("获取collections下标为2的元素:" + jedis.lindex("collections", 2));
    System.out.println("===============================");
    jedis.lpush("sortedList", "3", "6", "2", "0", "7", "4");
    System.out.println("sortedList排序前:" + jedis.lrange("sortedList", 0, -1));
    System.out.println(jedis.sort("sortedList"));
    System.out.println("sortedList排序后:" + jedis.lrange("sortedList", 0, -1));
  }
}

对 Set 的操作命令

public class TestSet {
  public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.flushDB();
    System.out.println("============向集合中添加元素(不重复)============");
    System.out.println(jedis.sadd("eleSet", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
    System.out.println(jedis.sadd("eleSet", "e6"));
    System.out.println(jedis.sadd("eleSet", "e6"));
    System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
    System.out.println("删除一个元素e0:" + jedis.srem("eleSet", "e0"));
    System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
    System.out.println("删除两个元素e7和e6:" + jedis.srem("eleSet", "e7", "e6"));
    System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
    System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet"));
    System.out.println("随机的移除集合中的一个元素:" + jedis.spop("eleSet"));
    System.out.println("eleSet的所有元素为:" + jedis.smembers("eleSet"));
    System.out.println("eleSet中包含元素的个数:" + jedis.scard("eleSet"));
    System.out.println("e3是否在eleSet中:" + jedis.sismember("eleSet", "e3"));
    System.out.println("e1是否在eleSet中:" + jedis.sismember("eleSet", "e1"));
    System.out.println("e1是否在eleSet中:" + jedis.sismember("eleSet", "e5"));
    System.out.println("=================================");
    System.out.println(jedis.sadd("eleSet1", "e1", "e2", "e4", "e3", "e0", "e8", "e7", "e5"));
    System.out.println(jedis.sadd("eleSet2", "e1", "e2", "e4", "e3", "e0", "e8"));
    System.out.println("将eleSet1中删除e1并存入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e1"));//移到集合元素
    System.out.println("将eleSet1中删除e2并存入eleSet3中:" + jedis.smove("eleSet1", "eleSet3", "e2"));
    System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1"));
    System.out.println("eleSet3中的元素:" + jedis.smembers("eleSet3"));
    System.out.println("============集合运算=================");
    System.out.println("eleSet1中的元素:" + jedis.smembers("eleSet1"));
    System.out.println("eleSet2中的元素:" + jedis.smembers("eleSet2"));
    System.out.println("eleSet1和eleSet2的交集:" + jedis.sinter("eleSet1", "eleSet2"));
    System.out.println("eleSet1和eleSet2的并集:" + jedis.sunion("eleSet1", "eleSet2"));
    System.out.println("eleSet1和eleSet2的差集:" + jedis.sdiff("eleSet1", "eleSet2"));//eleSet1中有,eleSet2中没有
    jedis.sinterstore("eleSet4", "eleSet1", "eleSet2");//求交集并将交集保存到dstkey的集合
    System.out.println("eleSet4中的元素:" + jedis.smembers("eleSet4"));
  }
}

对 Hash 的操作命令

public class TestHash {
  public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    jedis.flushDB();
    Map<String, String> map = new HashMap<String, String>();
    map.put("key1", "value1");
    map.put("key2", "value2");
    map.put("key3", "value3");
    map.put("key4", "value4");
    //添加名称为hash(key)的hash元素
    jedis.hmset("hash", map);
    //向名称为hash的hash中添加key为key5,value为value5元素
    jedis.hset("hash", "key5", "value5");
    System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));//return Map<String,String>
    System.out.println("散列hash的所有键为:" + jedis.hkeys("hash"));//return Set<String>
    System.out.println("散列hash的所有值为:" + jedis.hvals("hash"));//return List<String>
    System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 6));
    System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
    System.out.println("将key6保存的值加上一个整数,如果key6不存在则添加key6:" + jedis.hincrBy("hash", "key6", 3));
    System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
    System.out.println("删除一个或者多个键值对:" + jedis.hdel("hash", "key2"));
    System.out.println("散列hash的所有键值对为:" + jedis.hgetAll("hash"));
    System.out.println("散列hash中键值对的个数:" + jedis.hlen("hash"));
    System.out.println("判断hash中是否存在key2:" + jedis.hexists("hash", "key2"));
    System.out.println("判断hash中是否存在key3:" + jedis.hexists("hash", "key3"));
    System.out.println("获取hash中的值:" + jedis.hmget("hash", "key3"));
    System.out.println("获取hash中的值:" + jedis.hmget("hash", "key3", "key4"));
  }
}

对于事务的基本使用

public class TestTX {
  public static void main(String[] args) {
    Jedis jedis = new Jedis();
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("hello", "world");
    jsonObject.put("name", "ChangingTao");
    String result = jsonObject.toJSONString();
    // 开启事务
    Transaction multi = jedis.multi();
    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整合

SpringBoot 操作数据层:spring-data jpa、jdbc、mongodb、redis!

Spring Data 也是和 SpringBoot 齐名的项目!

说明: 在 SpringBoot 2.X 之后,原来使用的jedis被替换为 lettuce

jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool 连接池!更像 BIO 模式

lettuce:采用 netty,实例可以在多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像 NIO 模式

源码分析:

public class RedisAutoConfiguration {

  @Bean
  @ConditionalOnMissingBean(name = "redisTemplate") // 我们可以自己定义一个 redisTemplate 来替代这个默认的Bean
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    throws UnknownHostException {
    //  默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化!
    // 两个泛型都是 Object, Object 的类型,我们后面使用需要强制转换 <String, Object>
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
  }

  @Bean
  @ConditionalOnMissingBean // 由于 String 是 redis 中最常使用的类型,所以单独提出来了一个bean
  public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
    throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
  }

}

整合测试

1、导入依赖

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

2、配置连接

# SpringBoot 所有的配置类,都有一个自动配置类 RedisAutoConfiguration
# 自动配置类都会绑定一个 properties 配置文件 RedisProperties
spring.redis.host=127.0.0.1
spring.redis.port=6379
# 注意配置连接池需要使用 lettuce,不能使用 Jedis ,因为源码中 Jedis 很多都失效(SpringBoot2.x以上默认使用 Lettuce,没有导入 Jedis的包)

3、测试!

@SpringBootTest
class Redis02SpringbootApplicationTests {

  @Autowired
  private RedisTemplate redisTemplate;

  @Test
  void contextLoads() {
    // redisTemplate 操作不同的数据类型,api和我们的指令是一样的
    // opsForValue   操作字符申 类似 string
    // opsForList    操作list  类似 List
    // opsForSet
    // opsForHash
    // opsForZSet
    // opsForGeo
    // opsForHyperLogLog
    
    redisTemplate.opsForValue().set("mykey", "kuangshen");
    redisTemplate.opsForValue().get("mykey");

    /* 除了基本的操作,我们常用的方法都可以直接通过 redisTemplate 操作,比如事务,和基本的CRUD

        redisTemplate.multi();
        redisTemplate.discard();
        redisTemplate.exec();
        */

    /* 获取 redis 的连接对象

        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        connection.flushDb();
        connection.flushAll();
        */
  }

}

image.png

image.png

关于对象的保存:

  • 解决方法:
    • 将对象转成 Json 格式的字符串进行存储
    • 将对象的实体类接入序列化接口( implements Serializable

image.png

SpringBoot Redis自动配置类(RedisAutoConfiguration),我们可以自己编写 RedisTemplate 来覆盖原本的 RedisTRemplate ,自己配置序列化类型

@Configuration
public class RedisConfig {
  // 自己定义了一个新的RedisTemplate
  @Bean
  @SuppressWarnings("all")
  public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory)
    throws UnknownHostException {
    // 我们为了开发方便,一般直接使用 <String, Object>
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);

    /* 序列化配置 */
    // JSON 的序列化配置
    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);
    // String 的序列化配置
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();


    /* 配置具体的序列化方式 */
    // key采用 String 的序列化方式
    template.setKeySerializer(stringRedisSerializer);
    // hash的 key 也采用 String 的序列化方式
    template.setHashKeySerializer(stringRedisSerializer);
    // value 序列化方式采用 Jackson
    template.setValueSerializer(jackson2JsonRedisSerializer);
    // hash 的value序列化方式采用 Jackson
    template.setHashValueSerializer(jackson2JsonRedisSerializer);
    template.afterPropertiesSet();

    return template;
  }
}

因为频繁的调用原生的 redisTemplate 会比较繁琐且自由度不高,所以在公司企业中,一般都是存在一个对应的 RedisUtils,方便我们进行 Redis 操作。

@Component
public final class RedisUtil {

  @Autowired
  private RedisTemplate<String, Object> redisTemplate;

  // =============================common============================
  /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
  public boolean expire(String key, long time) {
    try {
      if (time > 0) {
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }

  /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
  public long getExpire(String key) {
    return redisTemplate.getExpire(key, TimeUnit.SECONDS);
  }


  /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
  public boolean hasKey(String key) {
    try {
      return redisTemplate.hasKey(key);
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
  @SuppressWarnings("unchecked")
  public void del(String... key) {
    if (key != null && key.length > 0) {
      if (key.length == 1) {
        redisTemplate.delete(key[0]);
      } else {
        redisTemplate.delete(CollectionUtils.arrayToList(key));
      }
    }
  }


  // ============================String=============================

  /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
  public Object get(String key) {
    return key == null ? null : redisTemplate.opsForValue().get(key);
  }

  /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

  public boolean set(String key, Object value) {
    try {
      redisTemplate.opsForValue().set(key, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

  public boolean set(String key, Object value, long time) {
    try {
      if (time > 0) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
      } else {
        set(key, value);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
  public long incr(String key, long delta) {
    if (delta < 0) {
      throw new RuntimeException("递增因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, delta);
  }


  /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
  public long decr(String key, long delta) {
    if (delta < 0) {
      throw new RuntimeException("递减因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, -delta);
  }


  // ================================Map=================================

  /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
  public Object hget(String key, String item) {
    return redisTemplate.opsForHash().get(key, item);
  }

  /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
  public Map<Object, Object> hmget(String key) {
    return redisTemplate.opsForHash().entries(key);
  }

  /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
  public boolean hmset(String key, Map<String, Object> map) {
    try {
      redisTemplate.opsForHash().putAll(key, map);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
  public boolean hmset(String key, Map<String, Object> map, long time) {
    try {
      redisTemplate.opsForHash().putAll(key, map);
      if (time > 0) {
        expire(key, time);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
  public boolean hset(String key, String item, Object value) {
    try {
      redisTemplate.opsForHash().put(key, item, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }

  /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
  public boolean hset(String key, String item, Object value, long time) {
    try {
      redisTemplate.opsForHash().put(key, item, value);
      if (time > 0) {
        expire(key, time);
      }
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
  public void hdel(String key, Object... item) {
    redisTemplate.opsForHash().delete(key, item);
  }


  /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
  public boolean hHasKey(String key, String item) {
    return redisTemplate.opsForHash().hasKey(key, item);
  }


  /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
  public double hincr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, by);
  }


  /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
  public double hdecr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, -by);
  }


  // ============================set=============================

  /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
  public Set<Object> sGet(String key) {
    try {
      return redisTemplate.opsForSet().members(key);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }


  /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
  public boolean sHasKey(String key, Object value) {
    try {
      return redisTemplate.opsForSet().isMember(key, value);
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
  public long sSet(String key, Object... values) {
    try {
      return redisTemplate.opsForSet().add(key, values);
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }


  /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
  public long sSetAndTime(String key, long time, Object... values) {
    try {
      Long count = redisTemplate.opsForSet().add(key, values);
      if (time > 0)
        expire(key, time);
      return count;
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }


  /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
  public long sGetSetSize(String key) {
    try {
      return redisTemplate.opsForSet().size(key);
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }


  /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

  public long setRemove(String key, Object... values) {
    try {
      Long count = redisTemplate.opsForSet().remove(key, values);
      return count;
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }

  // ===============================list=================================

  /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
  public List<Object> lGet(String key, long start, long end) {
    try {
      return redisTemplate.opsForList().range(key, start, end);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }


  /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
  public long lGetListSize(String key) {
    try {
      return redisTemplate.opsForList().size(key);
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }
  }


  /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
  public Object lGetIndex(String key, long index) {
    try {
      return redisTemplate.opsForList().index(key, index);
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }


  /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
  public boolean lSet(String key, Object value) {
    try {
      redisTemplate.opsForList().rightPush(key, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
  public boolean lSet(String key, Object value, long time) {
    try {
      redisTemplate.opsForList().rightPush(key, value);
      if (time > 0)
        expire(key, time);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }

  }


  /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
  public boolean lSet(String key, List<Object> value) {
    try {
      redisTemplate.opsForList().rightPushAll(key, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }

  }


  /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
  public boolean lSet(String key, List<Object> value, long time) {
    try {
      redisTemplate.opsForList().rightPushAll(key, value);
      if (time > 0)
        expire(key, time);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

  public boolean lUpdateIndex(String key, long index, Object value) {
    try {
      redisTemplate.opsForList().set(key, index, value);
      return true;
    } catch (Exception e) {
      e.printStackTrace();
      return false;
    }
  }


  /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

  public long lRemove(String key, long count, Object value) {
    try {
      Long remove = redisTemplate.opsForList().remove(key, count, value);
      return remove;
    } catch (Exception e) {
      e.printStackTrace();
      return 0;
    }

  }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值