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 | -k | 1=keep alive 0=reconnect | 1 |
8 | -r | SET/GET/INCR 使用随机 key, SADD 使用随机值 | |
9 | -P | 通过管道传输 <numreq> 请求 | 1 |
10 | -q | 强制退出 redis。仅显示 query/sec 值 | |
11 | –csv | 以 CSV 格式输出 | |
12 | -l | 生成循环,永久执行测试 | |
13 | -t | 仅运行以逗号分隔的测试命令列表。 | |
14 | -I | Idle 模式。仅打开 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 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
缺点
- 如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
- 这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
应用
- 消息订阅:公众号订阅,微博关注等等(其实更多是使用消息队列来进行实现)
- 多人在线聊天室。
稍微复杂的场景,我们就会使用消息中间件MQ处理