# Redis
## 1.概述
> Redis是什么
Redis(Remote Dictionary Server ),即远程字典服务
是一个开源的使用ANSI [C语言](https://baike.baidu.com/item/C语言)编写、支持网络、可基于内存亦可持久化的日志型、Key-Value[数据库](https://baike.baidu.com/item/数据库/103728),并提供多种语言的API
支持的语言
> Redis能干嘛
1.内存存储,持久化,内存中是断电及失
2.效率高,可以用于高速缓存
3.发布订阅系统
4.地图信息分析
5.计时器,计数器 (浏览量!)
> 特性
1.多样的数据类型
2.持久化
3.集群
4.事务
.....
`Redis推荐都是在Linux服务器上搭建,我们是基于Linux学习`
## 2.Linux安装
使用redis-cli连接
查看redis进程
关闭redis服务
查看进程是否存在
## 3.基础知识
1.使用select切换数据库
```bash
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> dbsize #查看DB大小
(integer) 0
```
不同的数据库存不同的值
2.获取所有的值
```bash
127.0.0.1:6379> keys * #查看所有的key
1) "name"
```
3.清除当前数据库
```bash
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
```
4.清除所有数据库类容
```bash
127.0.0.1:6379> flushall
OK
```
> Redis单线程
明白Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器内存和网络带宽,既然可以使用单线程来实现,就使用单线程
**Redis为什么单线程还这么快?**
1.误区1:高性能的服务器一定是多线程的?
2.误区2:多线程(CPU上下文会切换)效率一定比单线程高?
核心:redis是将所有的数据全部放在内存中的,所以说单线程去操作效率最高,多线程(CPU上下文切换:耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的
## 4.五大数据类型
### 1.Redis-Key
```bash
127.0.0.1:6379> set name shanshi
OK
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 age #查看key是否存在
(integer) 1 #存在返回1
127.0.0.1:6379> EXISTS age1
(integer) 0 #不存在返回0
127.0.0.1:6379> get name
"shanshi"
127.0.0.1:6379> EXPIRE name 5 #设置key过期时间,单位s
(integer) 1
127.0.0.1:6379> ttl name #查看过期还剩多少时间
(integer) -2 #-2代表已过期
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> set name xxx
OK
127.0.0.1:6379> type name #查看当前key的类型
string
127.0.0.1:6379> type age
string
127.0.0.1:6379> move name 1 #移动key到指定数据库
(integer) 1
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) "name"
```
### 2.String(字符串)
```bash
127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> append key1 spider #追加字符串,如果当前key不存在,就相当于set key
(integer) 8
127.0.0.1:6379> get key1
"v1spider"
#########################################################
#i++
#步长 i+=
127.0.0.1:6379> set views 0 #初始为0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> INCR views #自增1
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> DECR views #自减1
(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 11 #设置步长,指定增量
(integer) 10
127.0.0.1:6379> DECRBY views 20 #设置步长,指定减量
(integer) -10
#########################################################
#字符串范围 getrange
127.0.0.1:6379> set key1 "hello,Redis"
OK
127.0.0.1:6379> get key1
"hello,Redis"
127.0.0.1:6379> getrange key1 0 4 #截取字符串[0,4]
"hello"
#替换
127.0.0.1:6379> set key2 abcdefg
OK
127.0.0.1:6379> get key2
"abcdefg"
127.0.0.1:6379> setrange key2 2 C
(integer) 7
127.0.0.1:6379> get key2
"abCdefg"
127.0.0.1:6379> setrange key2 2 xx #替换指定开始的字符串
(integer) 7
127.0.0.1:6379> get key2
"abxxefg"
#########################################################
# setex (set with expire) #设置过期时间
# setnx (set if not exist) #不存在在设置(在分布式锁中会长使用)
127.0.0.1:6379> setex key3 30 "hello" #设置key3的值为hello,30秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 23
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx mykey "redis" #如果mykey不存在就创建
(integer) 1
127.0.0.1:6379> keys *
1) "mykey"
2) "key2"
3) "key1"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey "mysql" #如果mykey存在就创建失败
(integer) 0
127.0.0.1:6379> get mykey
"redis"
#########################################################
# mset
# mget
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #同时设置多个key value
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3 #同时获取多个value
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #msetnx 是原子行操作,要么一起成功,要么一起失败
(integer) 0
127.0.0.1:6379> keys * #key4 并没有创建成功
1) "k3"
2) "k1"
3) "k2"
#########################################################
#对象
set user:1 {name:zhangsan,age:3} #设置一个user:1对象 值为json字符串来保存对象
# 这里的key是一个巧妙的设计: user:{id}:{filed}
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 3
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "3"
#########################################################
# getset 先get在set
127.0.0.1:6379> getset sql mysql #如果没有sql这个key返回null,然后在set key
(nil)
127.0.0.1:6379> get sql
"mysql"
127.0.0.1:6379> getset sql orcale #如果存在key,获取原来的value,再set 新value
"mysql"
127.0.0.1:6379> get sql
"orcale"
```
数据结构是相同的!
String类似的使用场景:value可以是字符串,也可以是数字
- 计数器
- 统计多单位的数量
- 粉丝数
- 对象缓存存储
### 3.List(列表)
在redis里面,我们可以把list玩成栈、队列、阻塞队列
所有的list命令都是用l开头的,redis不区分大小写命令
```bash
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
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 #获取list中的值
1) "three"
2) "two"
127.0.0.1:6379> rpush list four #将一个值或多个值插入到列表的尾部(右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
#########################################################
# lpop rpop
127.0.0.1:6379> lpop list #移除list的第一个元素
"three"
127.0.0.1:6379> rpop list #移除list的最后一个元素
"four"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
#########################################################
# lindex
127.0.0.1:6379> lindex list 0 #获取list对应下标的值
"two"
127.0.0.1:6379> lindex list 1
"one"
#########################################################
# llen
127.0.0.1:6379> llen list #返回列表的长度
(integer) 2
#########################################################
# 移除指定的值 lrem
127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "three"
3) "one"
4) "two"
5) "one"
127.0.0.1:6379> lrem list 1 two #移除list集合中指定个数的value,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "three"
3) "one"
4) "one"
127.0.0.1:6379> lrem list 2 one
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "three"
#########################################################
# ltrim 截断list
127.0.0.1:6379> lrange mylist 0 -1
1) "hello3"
2) "hello2"
3) "hello1"
4) "hello0"
127.0.0.1:6379> ltrim mylist 1 2 # 通过下标截取指定的list长度,这个list已经被改变了
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello1"
#########################################################
# rpoplpush 移除列表的最后一个元素,将该值移动到新的列表中
127.0.0.1:6379> rpoplpush mylist list
"hello1"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello3"
2) "hello2"
127.0.0.1:6379> lrange list 0 -1
1) "hello1"
#########################################################
# lset
127.0.0.1:6379> exists mylist #判断列表是否存在
(integer) 1
127.0.0.1:6379> lrange mylist 0 -1
1) "hello3"
2) "hello2"
127.0.0.1:6379> lset mylist 0 "hello1" #更新当前下标的值
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
#########################################################
# linsert 将具体的value插入到列表中某个元素的前面或者后面
127.0.0.1:6379> linsert mylist before "hello1" "hello0" #在指定值的前面增加
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello0"
2) "hello1"
3) "hello2"
127.0.0.1:6379> linsert mylist after "hello2" "hello3" #在指定值的后面增加
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello0"
2) "hello1"
3) "hello2"
4) "hello3"
```
### 4.Set(集合)
set里面的值是不可以重复的
```bash
127.0.0.1:6379> sadd myset "xxp" #添加value
(integer) 1
127.0.0.1:6379> sadd myset "txl"
(integer) 1
127.0.0.1:6379> SMEMBERS myset #查看指定set的所有值
1) "txl"
2) "xxp"
127.0.0.1:6379> SISMEMBER myset "xxp" #判断元素是否存在set中,有返回1,无返回0
(integer) 1
127.0.0.1:6379> SISMEMBER myset "wjh"
(integer) 0
#########################################################
# scard
127.0.0.1:6379> scard myset #获取set中元素的个数
(integer) 3
#########################################################
# srem
127.0.0.1:6379> srem myset "wjh" #移除某一个元素
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "txl"
2) "xxp"
#########################################################
# set无序不重复集合,抽随机
127.0.0.1:6379> SRANDMEMBER myset #随机抽选出set中的一个元素
"xxp"
127.0.0.1:6379> SRANDMEMBER myset
"txl"
127.0.0.1:6379> SRANDMEMBER myset 2 #随机抽选出set中指定个数元素
1) "txl"
2) "xxp"
#########################################################
# 删除指定的key,随机删除key
127.0.0.1:6379> SMEMBERS myset
1) "wjh"
2) "txl"
3) "xxp"
127.0.0.1:6379> spop myset #随机删除set中的一个元素
"wjh"
127.0.0.1:6379> spop myset
"txl"
127.0.0.1:6379> SMEMBERS myset
1) "xxp"
#########################################################
# 将一个指定的值移动到另外一个set集合中
127.0.0.1:6379> sadd set1 "wjh"
(integer) 1
127.0.0.1:6379> sadd set1 "xxp"
(integer) 1
127.0.0.1:6379> sadd set2 "txl"
(integer) 1
127.0.0.1:6379> smove set1 set2 "xxp" #将指定的值移动到另一个集合中
(integer) 1
127.0.0.1:6379> SMEMBERS set1
1) "wjh"
127.0.0.1:6379> SMEMBERS set2
1) "txl"
2) "xxp"
#########################################################
# 交集,并集,补集
127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2 #以key1为参照物,查找差集
1) "a"
2) "b"
127.0.0.1:6379> SINTER key1 key2 #交集
1) "c"
127.0.0.1:6379> SUNION key1 key2 #并集
2) "c"
3) "e"
4) "b"
5) "d"
```
微博,某用户将所有关注的人放在一个集合中,将粉丝也放在一个集合中
### 5.Hash(哈希)
Map集合,key-map!,这时候这个值是map集合。本质和String类型没有区别
```bash
127.0.0.1:6379> hset myhash filed1 xxp #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash filed1 #获取一个字段值
"xxp"
127.0.0.1:6379> hmset myhash filed1 txl filed2 wjh #set多个key-value
OK
127.0.0.1:6379> hmget myhash filed1 filed2 #获取多个字段值
1) "txl"
2) "wjh"
127.0.0.1:6379> hgetall myhash #获取hash中全部的数据
1) "filed1"
2) "txl"
3) "filed2"
4) "wjh"
127.0.0.1:6379> hdel myhash filed1 #删除指定的key字段,对应的value值也就消失了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "filed2"
2) "wjh"
#########################################################
127.0.0.1:6379> hgetall myhash
1) "filed2"
2) "xxp"
3) "filed1"
4) "txl"
5) "filed3"
6) "wjh"
127.0.0.1:6379> hlen myhash #获取hash表的字段数量
(integer) 3
#########################################################
127.0.0.1:6379> HEXISTS myhash filed1 #判断hash中的指定字段是否存在
(integer) 1
127.0.0.1:6379> HEXISTS myhash filed4
(integer) 0
#########################################################
# 只获得所有的filed
# 只获得所有的value
127.0.0.1:6379> hkeys myhash
1) "filed2"
2) "filed1"
3) "filed3"
127.0.0.1:6379> hvals myhash
1) "xxp"
2) "txl"
3) "wjh"
#########################################################
# 没有decrby
127.0.0.1:6379> hset myhash filed5 5 #指定增量
(integer) 1
127.0.0.1:6379> HINCRBY myhash filed5 5
(integer) 10
127.0.0.1:6379> HINCRBY myhash filed5 -10
(integer) 0
127.0.0.1:6379> hsetnx myhash filed6 10 #如果不存在就设置
(integer) 1
127.0.0.1:6379> hsetnx myhash filed6 10 #如果存在就失败
(integer) 0
```
### 6.Zset(有序集合)
在set基础上,增加一个值,set k1 v1,zset k1 score1 v1
```bash
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) "two"
3) "three"
#########################################################
# 排序
127.0.0.1:6379> zadd salary 2500 xiaotang
(integer) 1
127.0.0.1:6379> zadd salary 5000 xiaoxiao
(integer) 1
127.0.0.1:6379> zadd salary 10000 xiaowang
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf #从最小值到最大值
1) "xiaotang"
2) "xiaoxiao"
3) "xiaowang"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores #显示所有用户并附带成绩
1) "xiaotang"
2) "2500"
3) "xiaoxiao"
4) "5000"
5) "xiaowang"
6) "10000"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf 5000 withscores #显示工资小于5000员工的升序排序
1) "xiaotang"
2) "2500"
3) "xiaoxiao"
4) "5000"
#########################################################
# 移除rem中的元素
127.0.0.1:6379> zrange salary 0 -1
1) "xiaotang"
2) "xiaoxiao"
3) "xiaowang"
127.0.0.1:6379> zrem salary xiaotang # 移除有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "xiaoxiao"
2) "xiaowang"
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
(integer) 1
127.0.0.1:6379> zadd myset 3 java
(integer) 1
127.0.0.1:6379> ZCOUNT myset 1 3 # 获取指定区间的成员数量
(integer) 3
127.0.0.1:6379> ZCOUNT myset 1 2
(integer) 2
```
## 5.三种特殊数据类型
### 1.geospatial(地理位置)
> geoadd
```bash
# geoadd 添加地理位置
# 有效的经度从-180度到180度
# 有效的维度从-85.05112878度到85.05112878度
#当坐标超出上述指定范围时,该命令会返回一个错误
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
(integer) 1
127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 108.96 34.26 xian
(integer) 1
```
> geopos
```bash
# 获得当前定位:一定是一个坐标值
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 shenzhen
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "114.04999762773513794"
2) "22.5200000879503861"
```
>geodist
两个人之间的距离!
单位:
- m表示单位为米
- km表示单位为千米
- mi表示单位为英里
- ft表示单位为英尺
```bash
127.0.0.1:6379> geodist china:city beijing shanghai km # 查看北京到上海的直线距离
"1067.3788"
```
>georadius 以给定的经纬度为中心,找出某一半径内的元素
```bash
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km #以110,30这个经纬度为中心,寻找方圆1000km内的城市
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
```bash
# 找出指定位置元素周围的其他元素
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字符串
```bash
# 将经纬度转换为一维的字符串
127.0.0.1:6379> geohash china:city beijing chongqi
1) "wx4fbxxfke0"
2) "wm5xzrybty0"
```
> geo的底层的实现原理其实就是zset,我们可以使用zset命令来操作geo
```bash
127.0.0.1:6379> ZRAnge china:city 0 -1 # 查看地图中全部元素
1) "chongqi"
2) "xian"
3) "shenzhen"
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) "shenzhen"
4) "hangzhou"
5) "shanghai"
```
### 2.hyperloglog
> 基数是什么?
(不重复的元素)
> 测试使用
```bash
127.0.0.1:6379> PFADD mykey a b c d e #创建第一组元素 mykey
(integer) 1
127.0.0.1:6379> PFCOUNT mykey #统计mykey元素中基数的数量
(integer) 5
127.0.0.1:6379> PFADD mykey2 d e f
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2
(integer) 3
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 #合并 mykey 和 mykey2 -> mykey3 (并集)
OK
127.0.0.1:6379> PFCOUNT mykey3 #查看并集数量
(integer) 6
```
### 3.bitmaps
> 位存储
bitmaps位图,都是操作二进制位来进行记录,只有0和1两个状态
使用bitmap来记录周一到周日的打卡
周一:1 周二:0 周三:0 周四:1 ...........
```bash
127.0.0.1:6379> SETBIT sign 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1
(integer) 0
127.0.0.1:6379> SETBIT sign 4 1
(integer) 0
127.0.0.1:6379> SETBIT sign 5 0
(integer) 0
127.0.0.1:6379> SETBIT sign 6 0
(integer) 0
```
查看某一天是否有打卡!
```bash
127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 6
(integer) 0
```
统计操作,统计打卡的天数!
```bash
127.0.0.1:6379> BITCOUNT sign #统计这周的打卡记录
(integer) 3
```
## 6.事务
Redis事务本质:一组命令的集合! 一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行!
一次性、顺序性、排他性 ! 执行一些列的命令!
```
-----------队列 set set set 执行------------
```
==redis事务没有隔离级别的概念==
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行! Exec
==redis单条命令是保存原子性的,但是事务不保证原子性!==
redis的事务:
- 开启事务(multi)
- 命令入队(........)
- 执行事务(exec)
> 正常执行事务
```bash
127.0.0.1:6379> MULTI #开启事务
OK
# 命令入队
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) "v2"
4) OK
```
> 放弃事务
```bash
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> DISCARD #取消事务
OK
127.0.0.1:6379> get k4 #事务对列中命令都不会被执行
(nil)
```
> 编译型异常(代码有问题!命令有错!) 事务中所有命令都不会被执行
```bash
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> getset k2 #错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec #执行事务报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1 #所有命令都不执行
(nil)
```
> 运行时异常(1/0),如果事务对列中存在语法性,那么执行命令的时候,`其他命令是可以正常执行的`,错误命令抛出异常
```bash
127.0.0.1:6379> set k1 "V1"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> 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"
```
> redis监视测试
正常执行成功!
```bash
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(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 80
2) (integer) 20
```
测试多线程修改值,使用watch可以当做redis的乐观锁操作!
```bash
127.0.0.1:6379> WATCH money #监视 money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> exec # 执行之前,另外一个线程修改了我们的值,这个时候会导致事务执行失败!
(nil)
```
这是事务还没有执行,插入线程二改变了money的值,导致事务执行失败了
如果修改失败,获取最新的值即可
```bash
127.0.0.1:6379> UNWATCH # 如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379> WATCH money # 获取最新的值,再次监视
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 100
QUEUED
127.0.0.1:6379(TX)> INCRBY one 100
QUEUED
127.0.0.1:6379(TX)> exec # 对比监视的值是否发生了变化,如果没有发生变化,那么可以执行成功,如果发生变化就执行失败
1) (integer) 900
2) (integer) 100
```
## 7.Jedis
使用java操作redis
### 1.什么是Jedis
Redis官方推荐的java连接开发工具
### 2.测试
1.导入对应的依赖
```xml
<!--导入jedis的包-->
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
</dependencies>
```
2.编码测试
```java
/**
* @author SanShi
* @date 2021/12/22
* @apiNote
**/
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());
}
}
```
输出:
常用的api和之前的指令一样
### 3.事务
```java
/**
* @author SanShi
* @date 2021/12/22
* @apiNote
**/
public class TestTx {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","SanShi");
jsonObject.put("password","123456");
//开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
multi.set("user",result);
//int i = 1/0; 代码抛出异常事务,执行失败
//执行事务
multi.exec();
}catch (Exception e){
//放弃事务
multi.discard();
e.printStackTrace();
}finally {
System.out.println(jedis.get("user"));
//关闭连接
jedis.close();
}
}
}
```
## 8.SprongBoot整合
在SpringBoot2.x之后,原来使用的jedis被替换为了lettuce
jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool连接池(更像BIO模式)
lettuce:采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数量(更像NIO模式)
源码分析:
```java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
// 我们可以自定义一个redisTemplate来替换这个默认配置
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
//默认的RedisTemplate没有过多的设置,redis对象都需要序列化!
//两个泛型都是Object, Object的类型,后面使用需要强制转换<String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
// 由于String是redis中最常使用的类型,所有说单独提出来一个bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
```
> 测试
### 1.导入依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
### 2.配置redis
```yaml
# springboot 所有配置类都有一个自动配置类 (RedisAutoConfiguration)
# 自动配置类都会绑定一个 properties (RedisProperties)
# 配置redis
spring:
redis:
port: 6379
host: 127.0.0.1
```
### 3.测试
```java
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate 操作不同的数据类型 api和指令是不一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List
redisTemplate.opsForValue().set("name","SanShi");
System.out.println(redisTemplate.opsForValue().get("name"));
}
```
关于对象的保存:
未序列化的对象
### 4.编写一个自己的RedisTemplate
```java
@Configuration
public class RedisConfig {
/**
* 固定模板,直接使用
* 编写自己的redisTemplate
**/
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// Json序列化配置
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jsonRedisSerializer.setObjectMapper(mapper);
// String序列化配置
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value的序列化方式采用Jackson
template.setValueSerializer(jsonRedisSerializer);
// hash的value序列化方式采用Jackson
template.setHashValueSerializer(jsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
```
> 使用自己编写的RedisTemplate
```java
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate 操作不同的数据类型 api和指令是不一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List
redisTemplate.opsForValue().set("name","SanShi");
System.out.println(redisTemplate.opsForValue().get("name"));
}
```
### 5.编写RedisUtil
```java
@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(String.valueOf(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;
}
}
}
```
> 测试使用
```java
@Autowired
RedisUtil redisUtil;
@Test
void test1(){
redisUtil.set("pwd","123456");
System.out.println(redisUtil.get("pwd"));
}
```
## 9.Redis.conf详解
启动的时候通过配置文件启动!
> 单位
1.配置文件对unit单位对大小写不敏感
> 包含
> 网络
```bash
bind 127.0.0.1 # 绑定的ip
protected-mode yes # 保护模式
port 6379 # 端口设置
```
> 通用 GENERAL
```bash
daemonize yes # 以守护进程的方式运行,默认是no,我们需要自己开启为yes
pidfile /www/server/redis/redis.pid #如果以后台的方式运行,我们就需要指定一个pid进程文件
# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境
# warning (only very important / critical messages are logged)
loglevel notice
logfile "/www/server/redis/redis.log" # 日志的文件位置名
databases 16 # 数据库的数量,默认是16个数据库
always-show-logo yes # 是否总是显示logo
```
> 快照 SNAPSHOTTING
持久化,在规定的时间内,执行了多少次操作!则会持久化到文件.rdb.aof
redis是内存数据库,如果没有持久化,那么数据断电及失!
```bash
# 如果900s之内,至少有1个key进行了修改,则进行持久化操作
save 900 1
# 如果300s之内,至少有10个key进行了修改,则进行持久化操作
save 300 10
# 如果60s之内,至少有10000个key进行了修改,则进行持久化操作
save 60 10000
top-writes-on-bgsave-error yes # 持久化如果出错,是否还需要继续工作!
rdbcompression yes #是否压缩rdb文件,需要消耗cpu资源!
rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验!
dir /www/server/redis/ #rdb文件保存的目录!
```
> 安全 SECURITY
```bash
127.0.0.1:6379> config get requirepass #获取redis的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456" #设置redis的密码
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> ping
(error) NOAUTH Authentication required. #发现所有命令都没有权限
127.0.0.1:6379> config get requirepass
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456 #使用密码进行登录
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
```
> 限制 CLIENTS
```bash
maxclients 10000 #设置能连接redis的最大客户端数量
maxmemory <bytes> #redis配置内存的最大容量
maxmemory-policy noeviction #内存达到上限之后的处理策略
```
> APPEND ONLY 模式 aof配置
```bash
appendonly no #默认是不开启aof模式的,默认使用rdb方式持久化的,在大部分情况下,rdb完全够用
appendfilename "appendonly.aof" #持久化文件的名字
# appendfsync always #每次修改都执行sync,消耗性能
appendfsync everysec #每秒执行一次sync,可能会丢失这1s的数据!
# appendfsync no #不执行sync,这个时候操作系统自己同步数据,速度最快
```