Redis

Redis

概述

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

redis是什么:

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

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

redis能干什么

  • 内存存储,持久化;内存是断电即失的,所以持久化很重要(rdb、aof)
  • 效率高,可以用于高速存储
  • 发布订阅系统(可以充当简单的消息队列)
  • 计时器(短信失效)、计数器(微博订阅量)
  • 地图信息分析

特点

  • 多样的数据类型
  • 持久化
  • 集群
  • 事务

学习网站

官网:https://redis.io/

中文网:http://www.redis.cn/

windows版本在GitHub上下载(停更很久了)

Redis推荐在Linux服务器上搭建

redis的存储类型:

  • 键的类型:字符串
  • 值的类型
    • 字符串 string
    • 哈希 hash
    • 列表 list
    • 无序集合 set
    • 有序集合 zset
安装
windows下安装

地址:https://github.com/dmajkic/redis

1、解压安装包

在这里插入图片描述

2、开启redis-server.exe

3、启动redis-cli.exe测试

在这里插入图片描述

linux下安装
# 1、下载安装包一般是放在 /opt目录下
[root@iZ2zeghyi5idbl1rlnkp5tZ opt]# wget http://download.redis.io/releases/redis-6.0.6.tar.gz

# 2、解压
[root@iZ2zeghyi5idbl1rlnkp5tZ opt]# tar xzf redis-6.0.6.tar.gz

# 3、接入解压后的文件,编译redis
[root@iZ2zeghyi5idbl1rlnkp5tZ opt]# cd redis-6.0.6
[root@iZ2zeghyi5idbl1rlnkp5tZ redis-6.0.6]# make

# 4、编译后src目录下就有了 redis-server了,复制一份redis.conf 文件,我们之后启动用我们复制的这个配置文件,原来的做保险
[root@iZ2zeghyi5idbl1rlnkp5tZ redis-6.0.6]# mkdir txconfig
[root@iZ2zeghyi5idbl1rlnkp5tZ redis-6.0.6]# cp /opt/redis-6.0.6/redis.conf txconfig/

# 5、修改配置文件,如下图所示,将 daemonize no 改成 daemonize yes

在这里插入图片描述

# 6、用修改后的配置文件启动redis,注意不要进入src目录下启动redis-server
[root@iZ2zeghyi5idbl1rlnkp5tZ redis-6.0.6]# src/redis-server /opt/redis-6.0.6/txconfig/redis.conf

在这里插入图片描述

# 7、使用redis客户端进行连接,注意输入密码:默认123456

在这里插入图片描述

# 8、查看redis进程是否开启
[root@iZ2zeghyi5idbl1rlnkp5tZ ~]# ps -ef|grep redis

在这里插入图片描述

# 9、关闭redis服务 shutdown后在ctrl+c退出或者exit退出,直接退出只是关闭客户端并没有关闭服务

在这里插入图片描述

问题

在centos7下编译redis6.0版本,如果出现以下错误

In file included from server.c:30:0:
server.h:1022:5: error: expected specifier-qualifier-list before ‘_Atomic’
     _Atomic unsigned int lruclock; /* Clock for LRU eviction */
#请先检查gcc的版本是否低于5
[root@iZ2zeghyi5idbl1rlnkp5tZ bin]# gcc -v
#如果低于5需要升级
[root@iZ2zeghyi5idbl1rlnkp5tZ bin]# sudo yum install centos-release-scl
[root@iZ2zeghyi5idbl1rlnkp5tZ bin]# sudo yum install devtoolset-8-toolchain

其中 devtoolset 的不同版本也对应的gcc的不同版本,例如在当前文章成文时:
devtoolset-3-toolchain 对应gcc 4.9.2
devtoolset-4-toolchain 对应gcc 5.3.1
devtoolset-6-toolchain 对应gcc 6.3.1
devtoolset-7-toolchain 对应gcc 7.3.1
devtoolset-8-toolchain 对应gcc 8.2.1
没有devtoolset-5-toolchain

#可以用以下命令启用高版本gcc:
[root@iZ2zeghyi5idbl1rlnkp5tZ bin]# sudo scl enable devtoolset-8 bash
docker安装
[root@iZ2zeghyi5idbl1rlnkp5tZ ~]# docker pull redis
[root@iZ2zeghyi5idbl1rlnkp5tZ ~]# docker run -p 6379:6379 -v /kgc/myredis/data:/data 
-v /kgc/myredis/conf/redis.conf:/usr/local/etc/redis/redis.conf -d redis:latest redis-server /usr/local/etc/redis/redis.conf --appendonly yes --requirepass "123456"
测试性能

redis-benchmark:官方自带的测试工具,也在src里

序号选项默认值描述
1-h指定服务器主机名127.0.0.1
2-p指定服务器端口6379
3-s指定服务器 socket
4-c指定并发连接数50
5-n指定请求数10000
6-d以字节的形式指定 SET/GET 值的数据大小2
7-k1=keep alive 0=reconnect1
8-rSET/GET/INCR 使用随机 key, SADD 使用随机值
9-P通过管道传输 <numreq> 请求1
10-q强制退出 redis。仅显示 query/sec 值
11–csv以 CSV 格式输出
12-l生成循环,永久执行测试
13-t仅运行以逗号分隔的测试命令列表。
14-IIdle 模式。仅打开 N 个 idle 连接并等待。

简单测试

# 100个并发连接,10000个请求数
[root@iZ2zeghyi5idbl1rlnkp5tZ redis-6.0.6]# src/redis-benchmark -h localhost -p 6379 -c 100 -n 10000

在这里插入图片描述

基础知识

Redis默认有16个数据库,在配置文件里可以看到;默认使用的是第一个,可以使用 select 进行切换

在这里插入图片描述

127.0.0.1:6379> select 3 		#切换到第3个数据库
OK
127.0.0.1:6379[3]> DBSIZE		#查看当前数据库大小
(integer) 0
127.0.0.1:6379[2]> flushdb		#清除当前数据库
OK
127.0.0.1:6379> FLUSHALL		#清除所有数据库(不管在哪个数据库使用,都是清除所有的数据库)
OK

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

Redis为什么单线程还这么快

误区一:高性能的服务器不一定就是多线程的

误区二:多线程(CPU上下文会切换,竞争条件:耗时操作)一定比单线程效率高

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗

4、使用多路I/O复用模型,非阻塞IO

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

Redis五种基本数据类型

网站:http://doc.redisfans.com/

Redis-Key(key是String类型的)
命令含义
DEL key [key…]根据单个或多个key删除键值对
EXIST [key…]查看键是否存在,返回存在的个数;如 exist key1 key2,都存在就返回2,1个存在就返回1
EXPIRE key seconds根据key设置过期时间
KEYS pattern根据表达式查找key,如:keys *查找所有key,KEYS h?llo 匹配 hello,hillo等
MOVE key db将当前数据库的 key 移动到给定的数据库 db 当中
TTL key根据key查看键值对剩余时间,key不存在返回-2;key永久存在返回-1
TYPE key返回 key 所储存的值的类型
String

基本命令

命令含义
SET key value没有这个 key 的时候会新建;有这个 key 会更改值
SETNX key value当key不存在时,才设置key的值为value,set if not exist;分布式锁
SETEX key time value设置值并设置过期时间,set with expire
MSET key value…设置多个键值对
MSETNX key value…同时设置多个键值对,仅当key不存在时;原子性的,即使多个键值对只有一个已经存在,别的也不会设置成功
GET key根据 key 获取值
MGET key…根据多个 key 获取多个值
GETSET key value将给定 key 的值设为 value ,并返回 key 的旧值(old value)
APPEND key value在原 key 的 value 后追加值,key不存在相当于 SET key
STRLEN key返回 key 所储存的字符串值的长度
INCR key将 key 中储存的数字值增一;如果 key 不存在新建一个值为0的key,在执行 incr
DECR key将 key 中储存的数字值减一;。。。
INCRBY key increment将 key 所储存的值加上增量 increment;。。。
DECRBY key increment将 key 所储存的值减去减量 increment;。。。
GETRANGE key start end截取 key 的值(包括start和end),负数表示从字符串最后开始计数,0 -1表示所有
SETRANGE key offset value用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始,value多长覆盖多长

进阶玩法

# 在Redis中存储一个对象:user1  name=zhangsan age=20

# 一般可以将 user1作为key ,后面的属性写成 json字符串作为value
127.0.0.1:6379> set user:1 {name:zhangsan,age:4}
OK
127.0.0.1:6379> get user:1
"{name:zhangsan,age:4}"

# 但是这样设计它单个的属性就拿不出来了,我们可以像下面这样设置,设置多个key但是都是以user:1开头的
# user:{id}:{field}
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 4
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "4"
List

在这里插入图片描述

在Redis中,我们可以把list玩成栈(先进后出)、队列(先进先出)、阻塞队列

消息队列:LPUSH RPOP 栈:LPUSH LPOP

命令含义
LPUSH key value…从左边插入数据(后插的在前面),先进后出
RPUSH key value…从右边插入数据(顺序插入),先进先出
LSET key index value根据下标修改key中的值,从0开始,列表必须存在
LINSERT key BEFORE|AFTER pivot value将值 value 插入到列表 key 当中,位于值 pivot 之前或之后
LPOP key移除并返回列表 key 的头元素
RPOP key移除并返回列表 key 的尾元素
RPOPLPUSH key1 key2将列表key1的最后一个元素移除,在添加到key2的第一个
LREM key count value删除key中的count数量的value,count>0正着删,count<0倒着删,count=0,删除所有等于value的值
LINDEX key index根据下标获取列表 key 的值,从0开始
LRANGE key start stop根据下标查询key中的值,从0开始,-1表示列表最后一个元素
LLEN key返回列表 key 的长度
LTRIM key start stop对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除
Set

set中的元素时不能重复的,且是无序的

命令含义
SADD key member…将一个或者多个 member 元素加入到集合key中
SMEMBERS key查询集合中所有值
SISMEMBER key member判断集合中是否有这个vlaue,有返回1,没有0
SREM key member…删除一个或者多个元素
SCARD key返回集合中元素个数
SMOVE key1 key2 member移动集合中的元素到另一个集合中
SRANDMEMBER key [count]如果命令执行时,只提供了 key 参数,那么返回集合中的一个随机元素,count参数上网站看
SPOP key移除并返回集合中的一个随机元素
SDIFF key…返回一个集合的全部成员,该集合是所有给定集合之间的差集
SINTER key…返回一个集合的全部成员,该集合是所有给定集合的交集
SUNION key…返回一个集合的全部成员,该集合是所有给定集合的并集

微博,将所有人关注的人放在一个set集合中,就可以获取到共同关注;还有共同好友,共同爱好等等

Hash

key-map,命令基本和key和string的命令一样,就是前面加了个h

命令含义
HSET key field value将哈希表 key 中的域 field 的值设为 value;创建或更新
HMSET key field1 value1 field2 value2设置多个值,hset也可以;现在hmset已被弃用
HSETNX key field value将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在
HGETALL key返回哈希表 key 中,所有的域和值
HKEYS key返回哈希表 key 中,所有的域
HVALS key返回哈希表 key 中,所有域的值
HGET key field获取指定key的指定field的值
HMGET key field…获取多个值
HDEL key field…删除哈希表 key 中的一个或多个指定域
HEXISTS key field查看哈希表 key 中指定的域存不存在
HLEN key查看哈希表 key 中的域有几个
# Hash 可以用来存储变更的数据,如用户信息等经常变更的信息,用String也可以但是冒号太多了,用Hash存就更方便了,String更适合存储字符串
127.0.0.1:6379> HSET user:1 name zs age 20
(integer) 2
Zset

有序set,在set基础上增加了score

命令含义
ZADD key score1 member1 score2 member2添加元素,根据score排序,score越小,越靠前
ZRANGE key start stop [WITHSCORES]获取索引区间的元素,加上withscores就连score一起返回
ZREVRANGE key start stop [WITHSCORES]获取索引区间的元素,从大到小,倒序
ZRANGEBYSCORE key min max [WITHSCORES]获取介于score值min和max之间的元素,默认闭区间,在score前加(表示开区间,min和max可以是-inf和+inf,表示查询所有(负无穷和正无穷)
ZREM key member…删除元素
ZCARD key返回集合元素个数
ZCOUNT key min max返回区间元素个数

案例思路:有序set 存班级成绩表,工资排序表

普通消息:1 重要消息:2,带权重进行判断

排行榜应用实现

三种特殊数据类型
Geospatial 地理位置

朋友的定位,附近的人,打车距离计算
Redis的Geo在Redis3.2版本就推出了,可以用来推算地理位置的信息,两地之间的距离,方圆几里的人

查询城市经纬度网站:https://jingweidu.bmcx.com/

只有六个命令,在redis官网查看

GEOADD

添加地理位置

规则:南北两级的地理位置无法直接添加,我们一般会直接下载城市数据,通过java程序一次性导入,这里测试用的手动输入

有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。当坐标位置超出上述指定范围时,该命令将会返回一个错误;横纬竖经,初中地理常识了解一下

# GEOADD key longitude(经度) latitude(维度) member
127.0.0.1:6379> GEOADD CHINA:city 118.8921 31.32751 nanjing 116.23128 40.22077 beijing 121.48941 31.40527 shanghai
(integer) 3
127.0.0.1:6379> GEOADD CHINA:city 113.27324 23.15792 guangzhou 113.88308 22.55329 shenzhen
(integer) 2
GEOPOS

从key里返回所有给定位置元素的位置

# GEOPOS key member...
127.0.0.1:6379> GEOPOS CHINA:city nanjing beijing
1) 1) "118.89209836721420288"
   2) "31.32750976275760735"
2) 1) "116.23128265142440796"
   2) "40.22076905438526495"
GEODIST

返回两个给定位置之间的距离

# GEODIST key member1 member2 [unit] 
指定单位的参数 unit 必须是以下单位的其中一个:
m 表示单位为米(不指定默认是m)
km 表示单位为千米
mi 表示单位为英里
ft 表示单位为英尺
127.0.0.1:6379> GEODIST CHINA:city nanjing beijing
"1017743.1413"
127.0.0.1:6379> GEODIST CHINA:city nanjing beijing km
"1017.7431"
GEORADIUS

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素

可以用来搜索附近的人,以自己为中心,搜索附近多少m的人

# GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
# m|km|ft|mi:半径单位
# WITHCOORD:返回坐标
# WITHDIST:返回距离
# WITHHASH:返回hash
# COUNT count:显示几个
127.0.0.1:6379> GEORADIUS CHINA:city 110 30 1000 km
1) "shenzhen"
2) "guangzhou"
3) "nanjing"
127.0.0.1:6379> GEORADIUS CHINA:city 110 30 1000 km withcoord withdist withhash count 1
1) 1) "guangzhou"
   2) "827.6084"
   3) (integer) 4046534010880445
   4) 1) "113.27324062585830688"
      2) "23.1579209662846921"
GEORADIUSBYMEMBER

这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点

# GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
127.0.0.1:6379> GEORADIUSBYMEMBER CHINA:city nanjing 1000 km
1) "nanjing"
2) "shanghai"
GEOHASH

返回11个字符的Geohash字符串

# GEOHASH key member...
127.0.0.1:6379> GEOHASH CHINA:city beijing nanjing
1) "wx4sucvncn0"
2) "wtsd1qyxfx0"
底层

GEO底层其实是一个 zset 所以它虽然没有删除命令但是我们可以使用zset来删除

127.0.0.1:6379> ZRANGE CHINA:city 0 -1
1) "shenzhen"
2) "guangzhou"
3) "nanjing"
4) "shanghai"
5) "beijing"
127.0.0.1:6379> ZREM CHINA:city shenzhen guangzhou
(integer) 2
Hyperloglog

用来统计集合的基数,一个集合中是没有两个相同的元素的所以就是统计集合的个数

有0.81%错误率

应用:

网页的uv(独立访客,一个人只算一次访问),其实把用户都放入set集合中也是可以的,但是只是为了统计uv就要消耗大量的空间,不值得;hyperloglog它占的内存是固定的,可以用来访问这些,虽然有错误率但是访问这些不是太重要的数据很方便

# PFADD key members...    添加元素
127.0.0.1:6379> PFADD mykey a b c d e f g g			
(integer) 1
127.0.0.1:6379> pfadd mykey2 f g h i j k l
(integer) 1

# PFCOUNT key    统计基数
127.0.0.1:6379> PFCOUNT mykey
(integer) 7

# PFMERGE destkey sourcekey...  复制多个key生成一个新的key
127.0.0.1:6379> PFMERGE mykey3 mykey2 mykey		# 赋值mykey、mykey2生成mykey3
OK
127.0.0.1:6379> PFCOUNT mykey3				# 可以看到数量是12而不是14,因为f、g重复了
(integer) 12
Bitmaps

位图,数据结果,操作二进制位来进行记录,只有0和1两个状态

可以用来储存只需要两个状态的信息,如用户登录和未登录(0表示未登录,1已登录这样);每天打没打卡等,一年也就只需要365bit 1字节=8bit 一年也就只需要46个字节左右

# SETBIT key offset value    设置或者清空key的value(字符串)在offset处的bit值,offset只能为数字,且需要>=0,<=2^32,返回值是进行SETBIT 之前,该offset位的比特值

# GETBIT key offset			 返回key对应的string在offset处的bit值

127.0.0.1:6379> setbit year 0 1		# 第一天打卡了
(integer) 0
127.0.0.1:6379> setbit year 1 0		# 第二天没打卡
(integer) 0
127.0.0.1:6379> setbit year 2 1		# 第三天打卡了
(integer) 0
127.0.0.1:6379> getbit year 0		# 获取状态
(integer) 1
127.0.0.1:6379> BITCOUNT year 		# 总共只有两天打卡了
(integer) 2

# BITCOUNT key [start] [end]  统计字符串被设置为1的bit数,这里的start和end指的是字符串的位置而不是offset的位置,如下
127.0.0.1:6379> set user abc			# 设置字符串 user value为abc
OK
127.0.0.1:6379> bitcount user
(integer) 10
127.0.0.1:6379> bitcount user 0 0		# 0位指的是a,a的ASCII码为97也就是01100001,有三个1
(integer) 3
127.0.0.1:6379> bitcount user 0 1		# 0-1指的是ab,b的ASCII码为98也就是01100010,加上a的一共六个1
(integer) 6
127.0.0.1:6379> bitcount user 2 2
(integer) 4

# redis里的字符串都是可以使用bitmaps去操作的

# 'a'的ASCII码是 97。转换为二进制是:01100001,二进制中的每一位就是offset值啦,比如在这里 offset 0 等于 ‘0’ ,offset 1等于'1' ,offset2等于'1',offset 7 等于'1' ,没错,offset是从左往右计数的,也就是从高位往低位
127.0.0.1:6379> set user a     
OK

# 我们通过SETBIT 命令将 andy中的 'a' 变成 'b' 应该怎么变呢,也就是将 01100001 变成 01100010 (b的ASCII码是98)
127.0.0.1:6379> setbit user 6 1
(integer) 0
127.0.0.1:6379> setbit user 7 0
(integer) 1
127.0.0.1:6379> get user
"b"
事务

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

Redis事务没有隔离级别的概念:所有的命令在事务中,并没有被直接执行!只有发起执行命令的时候才执行!Exec

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

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

----- 队列:set set set -----

执行过程:

  • 开启事务(multi)
  • 命令入队
  • 执行事务(exec)
  • 取消事务(discard)
正常执行
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> get k3
QUEUED
127.0.0.1:6379> exec				# 执行事务
1) OK
2) OK
3) OK
4) "v3"
放弃事务
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> DISCARD				# 整个队列都不会执行
OK
事务异常

两种异常:编译时异常(命令有错误)和运行时异常

编译时异常:整个队列都不执行

运行时异常:出现异常的命令不执行,其他的执行

# 编译时异常
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						# 错误的命令
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> exec						# 整个队列都不执行
(error) EXECABORT Transaction discarded because of previous errors.

# 运行时异常
127.0.0.1:6379> set k1 "v1"					# 设置k1为字符串
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> incr k1						# incr命令只对整数+1,运行时会报错
QUEUED
127.0.0.1:6379> set k2 v2					# 正常的命令
QUEUED
127.0.0.1:6379> exec						# 另一个命令正常执行,这也就是Redis没有原子性
1) (error) ERR value is not an integer or out of range
2) OK
127.0.0.1:6379> get k2						# 能够获取到k2
"v2"
事务监控

Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断,可以用来实现乐观锁,不管事务成功还是失败,只要事务执行完都会取消所有监控

Unwatch 命令用于取消 WATCH 命令对所有 key 的监视

悲观锁:很悲观,认为什么时候都会出问题,无论做什么都会加锁

乐观锁:很乐观,认为什么时候都不会出问题,所以不会上锁;更新数据的时候去判断一下,在此期间是否有人修改过这个数据

  • 获取watch
  • 更新的时候比较watch

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 out			# 监视money out
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乐观锁操作
# 在finalshell创建两个端口,都连接上Redis
# 端口1,先不执行到队列
127.0.0.1:6379> watch money out
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
# 端口2,改变money的值
127.0.0.1:6379> set money 1000
OK
#然后在提交端口1,发现执行结果为null,没有改变
127.0.0.1:6379> exec
(nil)

Jedis

Jedis是Redis官方推荐的Java连接开发工具。要在Java开发中使用好Redis中间件,必须对Jedis熟悉才能写成漂亮的代码

依赖
<dependencies>
    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.3.0</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.73</version>
    </dependency>
</dependencies>
编码测试
public class TestJedis {
    public static void main(String[] args) {
        //1、new Jedis 对象:linux下的redis默认有防火墙需要改配置文件才能连接上,这里用的docker的redis
        Jedis jedis = new Jedis("39.102.58.153", 6379);

        //jedis 所有的命令就是我们之前学习的所有指令,所以之前学的指令很重要
        jedis.auth("123456");   //密码
        System.out.println(jedis.ping());   
        System.out.println(jedis.set("user","李四"));
    }
}
事务再次理解
public class Transactions {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("39.102.58.153", 6379);
        jedis.auth("123456");
        jedis.flushDB();
        //开启事务
        Transaction multi = jedis.multi();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name", "张三");
        jsonObject.put("age", 20);
        String s = jsonObject.toString();
        try {
            multi.set("user1", s);
            multi.set("user2", s);
            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"));
        }
    }
}

SpringBoot整合redis

SpringBoot操作数据:Spring-data jdbc mongodb redis等

SpringData也是和SpringBoot齐名的项目

说明:在SpringBoot2.x版本之后,原来使用的jedis被替换成了lettuce

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

lettuce:采用netty,实例可以在多个线程中共享,线程安全,可以减少线程数据,更像NIO模式

源码分析
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")	//我们可以自己定义一个redisTemplate来替换默认的模板
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    // 默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化的
    // 两个泛型都是 Object类型,我们使用需要强制转换 <String,Object>
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}
依赖
<!--两个依赖都可以,第二个可以在创建项目时直接在nosql里面勾选-->
<!--底层是jedis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
    <version>1.4.5.RELEASE</version>
</dependency>

<!--底层是lettuce-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件
# 需要配置的属性可以在springboot自动配置包里看
spring:
  redis:
    host: 39.102.58.153
    password: 123456
测试
@SpringBootTest
class RedisSpringbootApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void test01() {
        // redisTemplate 操作不同的数据类型,api和我们的指令是一样的(方法名可能不同)
        // opsForValue 操作字符串 类似String
        // opsForList 操作list  类似list
        // opsForSet 。。。

        //除了基本的操作,常用的方法可以直接通过redisTemplate操作,比如事务和基本的crud
        
        //获取redis的连接对象
        /*RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        connection.flushAll();
        connection.flushDb();*/
        redisTemplate.opsForValue().set("user","李四");
        System.out.println(redisTemplate.opsForValue().get("user"));
    }
}

默认的redisTemplate它存储的数据是被序列化的

在这里插入图片描述

我们可以看下RedisTemplate这个类它的序列化配置

在这里插入图片描述

存储对象
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
//Redis中存储对象时 pojo 是需要序列化的,可以使用jdk原生的序列化(Serializable),也可以使用json序列化
public class User implements Serializable {
    private String name;
    private Integer age;
}

@SpringBootTest
class RedisSpringbootApplicationTests {
    @Autowired
    private RedisTemplate redisTemplate;


    @Test
    void test01() {
        /* 如果实体类没有继承serializable可以使用json实现序列化
        String jsonUser = new ObjectMapper().writeValueAsString(new User("张三", 20));
        redisTemplate.opsForValue().set("user",jsonUser);
        System.out.println(redisTemplate.opsForValue().get("user"));
        */
        redisTemplate.opsForValue().set("user",new User("张三",20));
        System.out.println(redisTemplate.opsForValue().get("user"));

    }
}
自定义redisTemplete解决序列化问题

Redis序列化接口的子类

在这里插入图片描述

@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        //将默认的<Object, Object>改为<String, Object>方便工作使用,不用在强转了
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        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;
    }
}

自定义后,存储的值就不会变成序列化码了,实体类也可以直接进行存储,不用在实现序列化接口

公司用的Redis工具类

把RedisTemplate命令封装成自己的工具类,方便使用

@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((Collection<String>) 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;
        }

    }
}
redis增删改查

controller层

@RestController
public class UsersController {
    @Autowired
    private UsersMapper usersMapper;

    @Resource
    private RedisUtil redisUtil;
    //增
    @RequestMapping("/set")
    public void set(@RequestBody Users users) {
        redisUtil.set("users",users);
    }
    //查,restful风格
    @RequestMapping("/get/{key}")
    public Users get(@PathVariable("key") String key) {
        return (Users) redisUtil.get(key);
    }
    //删
    @RequestMapping("/del/{key}")
    public void del(@PathVariable("key") String key) {
        redisUtil.del(key);
    }
}

postman测试

在这里插入图片描述

整合mysql

访问顺序:访问数据库—>先访问redis数据库—>如果redis库里没有值—>再去访问mysql库—>并且把查到的值赋值到redis库中

@PostMapping("/findAll")
public List<Users> findAll() {
    List<Users> list = (List<Users>) redisUtil.get("users");
    if (list == null) {
        list = usersMapper.queryUsers();
        redisUtil.set("users", list);
    }
    return list;
}
缓存图

在这里插入图片描述

Redis发布订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息

Redis客户端可以订阅任意数量的频道

订阅/发布消息图

角色:消息发布者、频道、消息订阅者
在这里插入图片描述

命令及描述
# 订阅给定的一个或多个频道
SUBSCRIBE channel [channel..]

# 订阅一个或多个符合给定模式的频道
PSUBSCRIBE pattern [pattern..]

# 退订一个或多个频道
SUBSCRIBE channel [channel..]

# 退订一个或多个符合给定模式的频道
PUNSUBSCRIBE pattern [pattern..]

# 向指定频道发布消息,消息发送者
PUBLISH channel message

# 查看订阅与发布系统状态。
PUBSUB subcommand [argument [argument...]
测试
# 先创建一个端口进行订阅
127.0.0.1:6379> SUBSCRIBE liangudaren				# 订阅一个频道 liangudaren
Reading messages... (press Ctrl-C to quit)			# Ctrl-C 退出
1) "subscribe"					# 订阅成功
2) "liangudaren"				# 订阅的频道
3) (integer) 1

# 在创建一个端口进行发布信息
127.0.0.1:6379> PUBLISH liangudaren bangbangde
(integer) 1

# 订阅的窗口会多出来信息
127.0.0.1:6379> SUBSCRIBE liangudaren
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "liangudaren"
3) (integer) 1
1) "message"				# 类型
2) "liangudaren"			# 频道
3) "bangbangde"				# 发送的信息
原理

每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

在这里插入图片描述

客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。

缺点
  1. 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
  2. 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
应用
  1. 消息订阅:公众号订阅,微博关注等等(其实更多是使用消息队列来进行实现)
  2. 多人在线聊天室。

稍微复杂的场景,我们就会使用消息中间件MQ处理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值