Redis笔记

# 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,这个时候操作系统自己同步数据,速度最快
```

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值