Redis

Redis总结

1、Redis:数据库、缓存、消息中间件MQ
2、Redis默认16个数据库,Redis默认是单线程的,在内存上操作
3、单线程为什么效率高:
	因为Redis是基于内存操作的,cpu不是redis的瓶颈,内存和带宽才是,如果使用多线程,会涉及到线程的上下文切换,耗费时间
4、Redis的基本类型
	String:    set
	List列表:可以做消息队列,左进右出;栈,左进左出   lpush
	set集合:不重复,可以用来计数    sadd
	hash散列:适合存用户信息    hset
	Zset有序集合:排行榜应用   zadd
5、Redis的特殊类型
	geospatial(地理位置):朋友的定位,附近的人,打车距离的计算   geoadd
	hyperloglog基数统计:网站UV计算访问量 使用set会存储大量的用户id,hyperloglog只是计数,不存储用户ID,节省大量空间     pfadd
	bitmaps:数据结构 利用二进制记录事务的状态   打卡 记录每天的上班状态  0上班 1没上班    setbit
6、悲观锁:
	很悲观,无论什么时候,都会出问题,无论做什么都要加锁
7、乐观锁:
	很乐观,认为不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过数据,获取version,更新的时候比较version
	
8、Redis事务:
	redis单条命令是保持原子性的,但是事务不保证原子性
	编译型异常:命令有错,事务中所有的命令都不会被执行
	运行时异常:事务队列中存在语法错误,那么执行命令时其他命令可以正常执行,错误命令会返回异常

9、事务的四大特性:
	原子性:对数据库操作的最小单元
	一致性:事务中的增删改查,同时成功或失败
	隔离性:事务之间互不影响
	持久性:事务成功之后,数据持久到数据库中
10、Redis内存达到上限的六大策略:
	volatile-lru:只对设置了过期时间的key进行LRU(默认值) LRU:近期很少使用
	allkeys-lru : 删除lru算法的key   
	volatile-random:随机删除即将过期key   
	allkeys-random:随机删除   
	volatile-ttl : 删除即将过期的   
	noeviction : 永不过期,返回错误
11、Redis持久化
	rdb:
	默认是rdb,
	Redis会单独的fork一个子进程来进行持久化,会先将数据入到一个临时文件中,待持久化过程都结束了,在用这个临时文件替换上次持久化好的文件。
	整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。
	如果需要进行大规模的数据的恢复,却对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
   	RDB的缺点是最后一次持久化后的数据可能丢失。	
	aof:
	将我们的所有命令都记录下来,恢复的时候把这个文件全部执行一遍 
	每一次修改都同步,文件的完整性会更好
12、Redis订阅功能
13、Redis主从复制:
	数据的复制都是单向的,都是从主节点到从节点
	Master以写为主,Slave以读为主,80%都是读操作
	复制原理:
		全量复制:只要重新连到master主机,就会进行一次全量复制
		增量复制:当主从机再正常状态,主机写操作,会进行增量复制到从机,保证主从机数据的一致性
	哨兵模式:
		当主机宕机之后,自动选择主机的模式
		原理:
			假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover【故障转移】过程,仅仅是哨兵1主观的
			认为主服务器不可用,这个现象称为**主观下线**
			当后面的哨兵也检测到主服务器宕机时,那么哨兵就会进行一次投票,投票结果由一个哨兵发起,进行failover操作;
			切换成功后,就会通过发布订阅者模式,让各个哨兵把自己监控的从服务器切换成主机,这个过程叫做**客观下线**
14、缓存穿透和击穿(针对于某一个key的访问)
	缓存穿透:当用户去请求数据的时候,会先从缓存中寻找,如果没有,则会从数据库中寻找,如果有人恶意攻击,
		绕过缓存,去大量的请求数据库服务器,就会对数据库造成很大的压力,导致宕机,这种现象成为`缓存穿透`
	缓存击穿:当缓存中的一条数据,被请求了很多次,这条数据设置的过期时间60秒,当数据过期还没有重新set的时候,
		大量的用户请求就会绕过缓存,直接请求数据库服务器,也可能会造成数据库服务器的宕机,这种现象称为`缓存击穿`
	解决方案:
		设置热点key永不过期 (比较容易出问题,不可能永远不过期)
		加互斥锁,当请求绕过缓存,请求数据库服务器的时候,设置线程锁,只允许一个线程去请求数据库,其他线程进行等待
15、缓存雪崩(大量的key过期,或者宕机)
	当缓存中大量的key同时过期,或者redis宕机
	解决方案:
		redis的高可用,多设置几台redis服务器	
		限流降级,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如某一个key只允许一个线程去查询数据,写缓存,其他线程等待	
		数据预热,把可能的数据预先访问一遍,这样大部分的数据都会预设到缓存中,设置不同的过期时间,尽量均匀

Nosql概述

为什么要用Nosql?
大数据时代,传统的数据库已经很难应付大量的数据
大数据的IO压力下,表几乎无法更大,如果有几亿条数据,增加一个列,就意味着要修改上亿条数据。

在这里插入图片描述
为什么要用Nosql?
用户的个人信息、社交网络、地理位置。用户自己产生的数据,用户日志等爆发式增长!
这时候我们就需要使用Nosql数据库,Nosql数据库可以很好的处理以上情况

Nosql 不止是sql
泛指非关系型数据库
关系型数据库:表格 行 列

Nosql的特点

 1. 方便扩展(数据之间没有关系,很好扩展)
 2. 大数据量高性能(Redis一秒写8万次,读11万次,Nosql的缓存记录级,是一种细粒度的缓存,性能会比较高)
 3. 数据类型是多样型的(不需要事先设计数据库,如果数据量十分庞大,就没有办法很好的设计数据库了)
 4. 不仅仅是数据
 5. 没有固定的查询语言
 6. 键值对存储,列存储,文档存储,图形数据库(社交关系拓扑图)
 7. 最终一致性,过程允许有误差
 8. CAP定理和BASE(异地多活)
 9. 高性能,高可用,高可扩

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

  1. 海量Volume
  2. 多样Variety
  3. 实时Velocity

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

  1. 高并发
  2. 高可扩
  3. 高性能

真正的公司实践一定是Nosql+RDBMS(关系型数据库)一起使用

在这里插入图片描述在这里插入图片描述
NoSQL的四大分类

  1. 键值对
新浪:Redis
美团:Redis+Tair
阿里、百度:Redis+memeCache
  1. 文档型数据库(bson和json格式一样)
MongoDB(必须掌握)
是基于分布式文件存储的数据库,主要用来处理大量的文档。
MongoDB是介于关系型数据库和非关系型数据库的中间的产品
MongoDB是非关系型数据库中功能最丰富的,最像关系型数据库的数据库
  1. 列存储的数据库
HBase
分布式文件系统
  1. 图形关系数据库
存储社交网络拓扑图,存的不是图形,存的是社交网络关系、广告推荐等
Neo4j、InfoGrid

Redis

远程字典服务Remote Dictionary Server

redis能干嘛(数据库、缓存、消息中间件MQ

  1. 内存存储、持久化,内存是断电即失的,持久化很重要
  2. 效率高,可以用于告诉缓存
  3. 发布订阅系统,消息队列
  4. 地图信息分析
  5. 计时器、计数器(浏览量)

特性:

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

基于linux学习redis
windows下的redis有兼容性问题,不建议使用

windows下安装:

Linux下安装redis

  1. 官网下载安装包

  2. 解压安装包 tar -zxvf

  3. 基本的环境安装

      c++环境(redis是用c++写的):yum install gcc-c++  
      查看gcc版本:  gcc -v
      redis目录下安装执行安装命令 :make
    
  4. 默认安装路径为/usr/local/bin下

  5. 拷贝redis.conf文件到bin目录下,方便我们修改redis的配置

  6. redis默认不是后台启动的,我们需要修改配置文件

  7. 启动redis

    # redis-server redis.conf  启动redis-server服务 以redis.conf配置文件启动
    
  8. 启动客户端

    # redis-cli -p 6379
    
  9. 关闭redis

    # shutdown 
    # exit
    
  10. 后期我们会使用单机启动redis集群测试!

redis-benchmark性能测试工具

redis-benchmark是一个压力测试工具,管方自带的性能测试工具

可选参数如下:
在这里插入图片描述
简单测试下:

# 测试:100个并发连接 每个并发100000个请求
在linux中输入如下命令
# redis-benchmark -h localhost -p 6379 -c 100 -n 100000

在这里插入图片描述

redis的基础知识

1、redis默认有16个数据库
在这里插入图片描述

2、默认使用的是第0个数据库
3、可以使用select 数字 来切换数据库

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

4、redis是单线程的

原因:redis是基于内存操作的,CPU不是redis的瓶颈,redis的瓶颈时内存和带宽,既然可以使用单线程来
实现,就使用单线程了
redis时c语言写的,官方提供的数据为100000+的QPS,完全不比同key-value的MemeCache差
为什么单线程还那么快?
误区1:高性能的服务一定时多线程的?
误区2:多线程(CPU上下文切换,每切换一次需要1500纳秒)一定比单线程效率高?
核心:redis是将所有的数据全部放在内存中的,对于多线程,CPU会上下文切换耗费时间,对于内存系
统来说,没有上下文切换效率就是最高的

redis的基础语法

Redis-key

select 3 #切换数据库
set name wanghan  #set key为name 值为王罕的键值对
get name #获取key为name 的键值对
keys * #获取所有的key
flushdb #清空数据库
flushall #清空全部数据库
exists name #是否存在一个key名为name
move name #移除这个key
expire name 10 #设置这个key 10秒过期
ttl name #查看这个key的剩余寿命 值为-2时代表已过期
type name #查看这个key 的值类型

五大基本类型:
String:字符串
List:列表
Set:集合
Hashes:散列
Zset: 有序集合
三种特殊数据类型:
geospatial
hyperloglog
bitmaps

五大基本类型:

1、String

value除了字符串,也可以是数字

#####################################
append key " " #给key的值追加字符串  如果这个key不存在,则新建key-value
strlen key  #key值得字符串长度
#####################################
127.0.0.1:6379>  set cout 4   
OK
127.0.0.1:6379> incr cout   递增
(integer) 5
127.0.0.1:6379> decr cout 递减
(integer) 4
#步长:
127.0.0.1:6379> incrby count 10   步长10递增
(integer) 14
127.0.0.1:6379> decrby count 5   步长5递减
(integer) 9
127.0.0.1:6379> 
##################################
#截取字符串范围
127.0.0.1:6379> get name
"wanghan ai chenmeng"
127.0.0.1:6379> getrange name 2 5   #截取字符串范围
"ngha"
127.0.0.1:6379> getrange name 0 -1  # 0 -1代表全部
"wanghan ai chenmeng"
127.0.0.1:6379>      
##################################
#替换
127.0.0.1:6379> get name
"wanghan ai chenmeng"
127.0.0.1:6379> setrange name 3 xxx  #替换指定位置得字符串
(integer) 19
127.0.0.1:6379> get name
"wanxxxn ai chenmeng"       
#setex 如果存在 setex key 30 "hello"   # 如果存在,设置值为hello  30秒过期
#setnx 如果不存在   setnx key "hello"  # 如果不存在,设置key的值为hello,如果存在,则不修改key 的值(常用在分布式锁中)
##################################
#批量设置
#mset  key1 v1 key2 v2 key3 v3
#msetnx    key1 v1  key4 v4  #设置失败,因为存在key1,所以整条信息set失败,是原子性操作,一起成功一起失败
#################################
#对象
可以set对象,但还需要解析
这里有一个key的巧妙的设计:user:{id}{field},如此设计在redis中完全ok 的
127.0.0.1:6379> mset user:1:name wanghan user:1:age 23
OK
127.0.0.1:6379> mget user:1
1) (nil)
127.0.0.1:6379> mget user:1:name user:1:age
1) "wanghan"
2) "23"
127.0.0.1:6379> 
#################################
#先get再set
#getset    
127.0.0.1:6379> getset db redis    #如果没有值,则返回null,并set值
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db  mongodb   #如果有值,则获取值,再set新值
"redis"
2、list

在list中可以完成栈、队列、阻塞队列!

127.0.0.1:6379> lpush list one    左push   l代表left
(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    查询从左往右查    l代表list
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> rpush list aaa  右push,注意查询出来,该值的位置  r代表right
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "aaa"
##################################
弹出
127.0.0.1:6379> lpop list   左弹出第一个元素
"three"
127.0.0.1:6379> rpop list   右弹出右边第一个元素(最后一个元素)
"aaa"
127.0.0.1:6379> lrange list 0 -1   
1) "two"
2) "one"
##################################
lindex 下表取值
127.0.0.1:6379> lindex list 0  通过下标获取列表中的某一个值
"two"
##################################
llen list长度
127.0.0.1:6379> llen list
(integer) 2
#################################
移除某一个值
lrem
127.0.0.1:6379> lrem list 2 two    移除list中 2 个值为two的值
(integer) 1
####################################
ltrim截取
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
4) "hello4"
127.0.0.1:6379> ltrim mylist 1 2   通过下标截取list元素
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello2"
2) "hello3"
127.0.0.1:6379> 
########################################
rpoplpush  从一个list取最后一个值,左push到另一个list中
127.0.0.1:6379> rpoplpush mylist yourlist   从一个资源取出,存到另一个资源
"hello2"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello0"
2) "hello1"
127.0.0.1:6379> lrange yourlist 0 -1
1) "hello2"
127.0.0.1:6379> 
######################################
lset 更新一个list某下标的值,如果list不存在,或者不存在下标,则报错
127.0.0.1:6379> lset mylist 1 changename   更新某list中某下标的值 
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello0"
2) "changename"
######################################
linsert   插入
127.0.0.1:6379> linsert mylist before hello0 hello-1  将某个具体的值插入到列表中某个元素的前面或后面
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello-1"
2) "hello0"
3) "changename"

list小结:

  • 实际上就是一个链表,before node after right、left都可以插入值
  • 如果key不存在,则新建链表,如果存在,新增内容
  • 如果移除所有的值,空链表也代表不存在
  • 两边插入或改动值,效率最高;如果是操作中间元素,效率相对较低

消息队列(Lpush Rpop)
栈(Lpush Lpop)

3、Set(集合)

set中的值不能重复

新增
sadd myset "hello"
查看set
smembers myset
查看元素是否存在
smembers myset hello  如果存在返回1 不存在返回0
获取set中元素个数
scard myset
移除myset元素
srem myset hello
#########################################
set无序 不重复集合!抽随机
srandmember myset  随机抽取myset中的元素
srandmember myset 2  随机抽取myset中指定个数的元素
#########################################
随机的删除元素
spop myset
########################################
将集合中的某个元素移动到另一个集合
smove myset  youset hello
########################################
差集、并集、交集   微博B站共同关注
sdiff myset youset 查看 myset集合中与youset不同的元素(理解重点在myset的差集)
sinter myset youset 查看两者的交集
sunion myset youset 查看两者并集
4、Hash(哈希)

map集合 key - < key-value> key-map

127.0.0.1:6379> hset myhash name wanghan   设置一个集合 放入x
(integer) 1
127.0.0.1:6379> hget myhash name
"wanghan"
127.0.0.1:6379> hmset myhash age 13 sex man     #设置多个key-value
OK
127.0.0.1:6379> hmget myhash name age sex   #同时获取多个值
1) "wanghan"
2) "13"
3) "man"
127.0.0.1:6379>  hgetall myhash   #获取全部的数据
1) "name"
2) "wanghan"
3) "age"
4) "13"
5) "sex"
6) "man"
#############################################################
删除某一个key-value
127.0.0.1:6379>  hdel myhash name   #删除hash指定的key,对应的value也就删除了
(integer) 1
#############################################################
获取hash的长度,有多少个键值对
hlen myhash
#############################################################
判断hash中的某个key是否存在
hexists myhash name 
#############################################################
只获得key
hkeys myhash
#############################################################
只获得value
hvals myhash
#############################################################
给某个key的value值设置步长
127.0.0.1:6379> hset myhash num 1
(integer) 1
127.0.0.1:6379> hincrby myhash num 4  #key为num的值加4
(integer) 5
127.0.0.1:6379> hget myhash num
"5"
#############################################################
hsetnx myhash name wanghan  #如果不存在则设置,如果存在则报错

hash更适合存用户信息,经常变动的信息,String适合存字符串 
5、Zset(有序集合)

在set的基础上,增加了顺序标识

新增zset
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 5000 wanghan
(integer) 1
127.0.0.1:6379> zadd salary 2500 chenmeng
(integer) 1
127.0.0.1:6379> zadd salary 7000 wangjian
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf
1) "chenmeng"
2) "wanghan"
3) "wangjian"
###################################################
移除元素
127.0.0.1:6379> zrem salary wangjian   #移除元素
(integer) 1
查看多少个元素
127.0.0.1:6379> zcard salary     
(integer) 2
获取指定成员之间的元素个数
127.0.0.1:6379> zcount myset 3 6
(integer) 5

其余的API可以去查管方文档
案例:
set排序 排行榜应用

三种特殊数据类型:

1、geospatial(地理位置)

朋友的定位,附近的人,打车距离的计算
两地之间的距离,方圆几里的人
只有6个命令

#geoadd:添加地理位置
#规则:两极无法直接添加,我们一般会下载城市数据,直接通过java程序导入
127.0.0.1:6379> geoadd China:city 113.88 22.55 深圳     #geoadd key 经度 纬度 位置名称
(integer) 1
127.0.0.1:6379> geoadd China:city 104.10 30.65 成都 113.27 23.15 广州
(integer) 2
#geopos:查询地理位置坐标
127.0.0.1:6379> geopos China:city 北京
1) 1) "116.23000055551528931"
   2) "40.2200010338739844"
#geodist:两人之间的距离
单位:
 - m 米
 - km 千米
 - mi 英里
 - ft 英尺

127.0.0.1:6379> geodist China:city 北京 广州 km
"1918.8645"
#georadius:以给定的经纬度为中心,找出某一半径内的元素
#withdist :显示距离 withcoord:显示经纬度 cout 3:查找最多3个元素
#以110 30 经纬度为中心,查询出房源1000 km距离内的3个地理位置信息(距离信息,坐标信息)
127.0.0.1:6379> georadius China:city 110 30 1000 km withdist withcoord count 3
1) 1)"北京"
   2) "340.8679"
   3) 1) "106.54000014066696167"
      2) "29.39999880018641676"
2) 1) "上海"
   2) "570.9717"
   3) 1) "104.09999996423721313"
      2) "30.6499990746355806"
3) 1) "成都"
   2) "828.2964"
   3) 1) "113.27000051736831665"
      2) "23.14999996266175941"
#georadiusbymember 找出指定元素周围多少距离的元素
127.0.0.1:6379> GEORADIUSBYMEMBER China:city 北京 1000 km
1) "成都"
2) "重庆"
#geohash 将二维的经纬度,转化为一维的字符串,如果两个字符串越相似,那么距离越近
127.0.0.1:6379> geohash China:city 北京 成都
1) "wx4sucu47r0"
2) "wm6n2gem1v0"

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

zrange China:city 0 -1  查看地图中全部元素
zrem China:city 北京   移除地图中北京的元素
2、hyperloglog基数统计

什么是基数:就是只不重复的元素个数
比如{1,2,3,4,5,1,2,4,},这个集合中有8个元素,但是基数为5,另外3个元素与其中的元素重复,不计算在个数中

传统的网站UV计算访问量会有set集合
set集合放入用户ID,多次访问,只是用户ID只会覆盖,可以用来计数

使用hyperloglog的优点
在于内存的占用,2^64个元素,只需要12kb内存
不会像set那样,储存大量的用户ID,我们的目的是计数,而不是保存大量的用户ID

错误率在0.81% ,统计UV任务可以忽略不计

命令就3个

#pfadd  添加
#pfcount   计数key中元素个数
#pfmerge  新建key 并合并后面的n个key
127.0.0.1:6379> pfadd myset1 a b c d e f g 
(integer) 1
127.0.0.1:6379> pfadd myset2 h i j k a b c 
(integer) 1
127.0.0.1:6379> pfadd myset3 o p q r a b c 
(integer) 1
127.0.0.1:6379> pfcount myset 3
(integer) 0
127.0.0.1:6379> pfcount myset3
(integer) 7
127.0.0.1:6379> pfmerge myset myset1 myset2 myset3 
OK
127.0.0.1:6379> pfcount myset
(integer) 15
3、Bitmaps

Bitmaps 数据结构 利用二进制记录事务的状态 0 1
比如记录打卡天数

#setbit 
#记录一个月中的的打卡记录    1-5号每天的打卡记录
127.0.0.1:6379> setbit work 1 0
(integer) 0
127.0.0.1:6379> setbit work 2 0 
(integer) 0
127.0.0.1:6379> setbit work 3 1
(integer) 0
127.0.0.1:6379> setbit work 4 0
(integer) 0
127.0.0.1:6379> setbit work 5 1
(integer) 0
......
#getbite #获取某个记录
127.0.0.1:6379> getbit work 3   获取3号的打卡记录
(integer) 1
#bitcount  计算key中 全部元素值为1的元素个数
127.0.0.1:6379> bitcount work
(integer) 2

Redis的基本事务

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

redis事务没有隔离级别的概念
所有命令在事务中并没有被直接执行,只有发起执行命令的时候才会执行
redis单条命令是保持原子性的,但是事务不保证原子性

redis的事务:

  • 开启事务(multi)
  • 命令入队()
  • 执行事务(exec)
  • 放弃事务(discard)
127.0.0.1:6379> multi   #开启事务
OK
127.0.0.1:6379> set  key1 v1   #命令入队 QUEUED
QUEUED
127.0.0.1:6379> set key2 v2 
QUEUED
127.0.0.1:6379> set key3 v3 
QUEUED
127.0.0.1:6379> get key2
QUEUED
127.0.0.1:6379> exec  #执行事务
1) OK
2) OK
3) OK
4) "v2"
#discard 放弃事务
 127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key1 v1 
QUEUED
127.0.0.1:6379> set key4 v4 
QUEUED
127.0.0.1:6379> discard   #放弃事务
OK
127.0.0.1:6379> get key4   #命令未执行
(nil)

异常:

  • 编译型异常:命令有错,事务中所有的命令都不会被执行
127.0.0.1:6379> set k2 v2 
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3  #命令格式不正确
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4 
QUEUED
127.0.0.1:6379> exec  #执行事务报错,所有命令都不会被执行
(error) EXECABORT Transaction discarded because of previous errors.
  • 运行时异常: 事务队列中存在语法错误,那么执行命令时其他命令可以正常执行,错误命令会返回异常
127.0.0.1:6379> set k1 "string"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1    #对k1的值自增,应为值为字符串,所以语法会报错
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range   #返回异常信息
2) OK             #正常执行
3) "v2"

悲观锁:

  • 很悲观,无论什么时候,都会出问题,无论做什么都要枷锁

乐观锁:

  • 很乐观,认为不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过数据
  • 获取version
  • 更新的时候比较version

Redis的监控测试
监控money 正常执行成功

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
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

模拟多线程,当一个客户端开启事务对对象进行操作,如果过程中其他客户端,对该对象进行了修改,则本客户端在提交事务的时候,会执行失败

127.0.0.1:6379> get money   
"100"
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20   #如果在事务列队期间,其他线程对money进行了修改,由100增加到200
QUEUED
127.0.0.1:6379> INCRBY out 20 
QUEUED
127.0.0.1:6379> exec    #则在执行事务的时候会不成功
(nil)

原理:监视money,当开启事务的时候,money值为100(version),当执行事务的时候,再去判断money的值,如果值不是100,则事务执行失败。

127.0.0.1:6379> unwatch  #解锁

Jedis

我们要使用java来操作redis

 什么是Jedis,是redis官方推荐的java连接开发工具,使用java操作redis的中间件!
  1. 导包
		<dependency>
             <groupId>redis.clients</groupId>
             <artifactId>jedis</artifactId>
             <version>3.3.0</version>
         </dependency>
  1. 编码测试
 Jedis jedis = new Jedis("127.0.0.1", 6379);
 //jedis的所有命令就是我们之前学习的所有指令
 System.out.println(jedis.ping());
 jedis.close();
常用API:就是redis的操作指令
五大数据类型
三大特殊数据类型

Jedis中操作事务

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("a", "b");
        String result = jsonObject.toJSONString();
        
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        //开启事务
        Transaction multi = jedis.multi();
        try {
            multi.set("k1", result);
        } catch (Exception e) {
            multi.discard();
        }finally {
            multi.exec();
            System.out.println(jedis.get("k1"));
        }

springboot整合redis

说明:在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;
	}
	@Bean
	@ConditionalOnMissingBean //由于String是redis中最常使用的类型,所以单独提出来一个bean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

导包

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

配置redis

#springboot的所有配置类,都有一直自动配置类
#配置类都会绑定一个properties配置文件
#配置redis
spring:
  redis:
    host: 127.0.0.1
    port: 6379

测试
使用RedisTemplate来操作redis

 @Resource
    RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
        //opsForValue操作字符串 类似String
        redisTemplate.opsForValue().append("k1","v1");
        //opsForList()操作list的
        redisTemplate.opsForList().leftPop("k1");
        ....类似于redis的操作命令
    }

获取redis的连接对象

  //获取redis的连接对象
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        connection.flushDb();
        connection.flushAll();

自定义解决序列化问题(默认使用jdk序列化,修改后使用json序列化)

@Configuration
public class RedisConfig {

    //固定的模板,公司开发中,拿过去用就可以
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        //为了开发方便,直接修改成<String Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //配置Json序列化
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();// 需要导jackson的依赖
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //ke采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        //value采用jackson的序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
}

RedisUtils工具类
实际工作中常用的api都会封装到一个工具类中

Redis的配置文件

解析redis.conf配置文件

  • 单位,对大小写不敏感
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

  • 包含其他配置文件
################################## INCLUDES ###################################

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf
  • 网络
#后台方式启动
bind 127.0.0.1 #绑定IP
protected-mode yes  #开启保护模式
port 6379
  • 通用配置
#后台方式启动
daemonize yes
pidfile /var/run/redis_6379.pid   #如果以后台方式运行,我们就需要配置指定一个pid文件
loglevel notice  #日志级别
logfile "" #日志的文件位置名
databases 16 #默认16个数据库
always-show-logo yes #是否在启动的时候展示logo
  • 快照
#后台方式启动
持久化:在规定的时间内,执行了多少次操作,则会持久化到文件  .rdb .aof
持久化规则:redis内存数据库,如果没有持久化,断电即失,需要持久化
save 900 1    #如果900秒内,如果至少一个key进行了修改,我们即进行持久化操作
save 300 10 #如果300秒内,如果至少10个key进行了修改,我们即进行持久化操作
save 60 10000  # 如果在60秒内,如果由10000个key进行了修改,我们即进行持久化操作
#可以自己定义

stop-writes-on-bgsave-error yes #如果持久化出错,是否需要继续工作
rdbcompression yes  #是否压缩rdb(持久化)文件
rdbchecksum yes #保存rdb文件的时候,进行错误的检查校验
dbfilename dump.rdb #持久化文件名称
dir ./  #持久化文件保存目录
  • REPLICATION (主从复制)
replicaof <masterip> <masterport>  #设置为某个主机的从机
masterauth <master-password>   #主机的密码
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
  • SECURITY(安全)
# requirepass foobared   #连接默认无限制

#可以通过修改文件
requirepass  123456
#也可以通过redis命令行
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass root
OK
127.0.0.1:6379> config get requirepass   
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth root    #设置好之后需要输入密码登陆
OK
  • 客户端的限制
 maxclients 10000 #设置能连接上redis的最大客户端数量
 maxmemory <bytes> #redis配置最大内存容量
 maxmemory-policy noeviction  #当内存到大上限之后的处理策略
	六大策略:
		1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
		2、allkeys-lru : 删除lru算法的key   
		3、volatile-random:随机删除即将过期key   
		4、allkeys-random:随机删除   
		5、volatile-ttl : 删除即将过期的   
		6、noeviction : 永不过期,返回错误
  • aof 持久化模式 APPEND ONLY MODE
appendonly no   #默认是不开启aof模式的,默认使用rdb方式持久化,在大部分所有的情况下,rdb完全够用
appendfilename "appendonly.aof" #aof文件名称
appendfsync always       #每次修改都会同步,性能消耗严重
appendfsync everysec     #每秒执行一次同步,可能会丢失这1秒的数据
appendfsync no           #不执行同步,操作系统自己同步数据,速度最快

Redis持久化

RDB

在这里插入图片描述

   Redis会单独的fork一个子进程来进行持久化,会先将数据入到一个临时文件中,待持久化过程都结束了,
在用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高
的性能。如果需要进行大规模的数据的恢复,却对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
   RDB的缺点是最后一次持久化后的数据可能丢失。
   我们默认就是使用RDB模式,一般情况下不需要修改这个配置。

rdb保存的文件是dump.rdb
触发机制:

1、save的规则满足的情况下,会自动触发rdb规则
2、执行flushall命令,也会产生rdb文件
3、退出redis,也会产生rdb文件

如何恢复rdb文件

只需要将rdb文件放到redis启动目录就可以,redis启动的时候会自动检查dump.rdb文件,恢复其中的数据
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"    #这个目录就是配置文件目录

优点:

1、适合大规模的数据恢复!
2、如果你对数据的完整性要求不高

缺点:

1、需要一定的时间间隔操作持久化,如果redis意外宕机了,最后一次修改的数据可能就没有了
2、fork进程的时候,会占用一定的内容空间

AOF

append only file (追加内容)

将我们的所有命令都记录下来,恢复的时候把这个文件全部执行一遍

redis配置文件需要启用aof持久化,重启生效

appendonly yes

aof保存的文件是appendonly.aof

1、如果aof的文件有错误,这个时候redis是启动不起来的,我们需要修复这个aof文件
2、redis 给我们提供了一个工具  redis-check-aof  --fix   ;(其实就是删掉了出问题的数据)

[root@192 bin]# redis-check-aof --fix appendonly.aof 
0x              87: Expected \r\n, got: 6473
AOF analyzed: size=160, ok_up_to=110, diff=50
This will shrink the AOF from 160 bytes, with 50 bytes, to 110 bytes
Continue? [y/N]: y
Successfully truncated AOF

优点

1、每一次修改都同步,文件的完整性会更好
2、每秒同步一次,可能会丢失一秒的数据
3、从不同步,效率最高

缺点

1、相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
2、aof的运行效率也要比rdb慢,所以默认选择rdb

aof默认的是文件的无限追加
可以设置当aof文件大于一定数值之后,重写aof文件
重写:比如对一个对象进行两次追加,则合并成一个命令,追加一次

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

Redis订阅

命令:

1、subscribe  #订阅某个频道
2、publish  #发布消息
3、unsubscribe  # 取消订阅频道
4、psubscribe #订阅一个或多个给定模式的频道
5、pubsub #查看订阅与发布系统

订阅者

127.0.0.1:6379> subscribe wanghan
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "wanghan"
3) (integer) 1
1) "message"
2) "wanghan"
3) "wanghanaichenmeng"

发布者

127.0.0.1:6379> publish wanghan "wanghanaichenmeng"
(integer) 1

Redis主从复制

数据的复制都是单向的,都是从主节点到从节点
Master以写为主,Slave以读为主,80%都是读操作

主从 复制的作用:

1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式 
2、故障恢复
3、负载均衡
4、高可用

最基本的配置:一主二从

环境配置

1、多个redis服务器配置文件修改

127.0.0.1:6379> info replication
# Replication
role:master   #角色
connected_slaves:0   #没有从机
master_replid:222f6becba95af5041e3b67169969e80d92bd284
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

复制3个配置文件
redis79.conf
redis80.conf
redis81.conf
修改对应的信息

1、端口
2、pid文件名
3、log文件名
4、dump文件名

2、配置主从复制

默认情况下每台主机都是master主机
需要配置才能成为某台主机的从机
配置从机命令salveof 
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379    #成为ip 端口号的从机
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:cec44e4fcca81d6701cf7578e9dcb8d1c5a9e0f4
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
127.0.0.1:6380> 

真实的主从配置是再配置文件中进行配置的

replicaof <masterip> <masterport>  #设置为某个主机的从机
masterauth <master-password>   #主机的密码

复制原理:
全量复制:只要重新连到master主机,就会进行一次全量复制
增量复制:当主从机再正常状态,主机写操作,会进行增量复制到从机,保证主从机数据的一致性

思考:

链式主从结构:
下一个主机是自己的从机
第一个主机是master,其他的主机仍然为从机。
如果一个从机执行slaveof no one  那他就会从一个从机变为主机,他下面的从机还是他的从机 
当一个主机宕机时,从机可以执行 slave no one ,成为一个主机

哨兵模式

当主机宕机之后,自动选择主机的模式
在这里插入图片描述
哨兵是一个独立的进程,独立运行

原理:哨兵通过发送命令,等待redis服务响应,从而监控多个运行的redis实例
当一个哨兵监听多个redis实例,如果哨兵宕机,可能会出现问题,因此就需要搭建哨兵集群
哨兵集群和redis集群,相辅相成

在这里插入图片描述

	假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover【故障转移】过程,仅仅是哨兵1主观的认为主
服务器不可用,这个现象称为**主观下线**
	当后面的哨兵也检测到主服务器宕机时,那么哨兵就会进行一次投票,投票结果由一个哨兵发起,进行failover操作;
	切换成功后,就会通过发布订阅者模式,让各个哨兵把自己监控的从服务器切换成主机,这个过程叫做**客观下线**
  1. 创建哨兵的配置文件sentinel.conf文件
#配置哨兵监听主服务器的ip和端口 为监测的主机设置名称:myredis  1代表当主服务器宕机,实行选举模式
sentinel monitor myredis 127.0.0.1 6379 1 
  1. 启动哨兵
[root@192 bin]# redis-sentinel redisconf/sentinel.conf 
14533:X 21 Nov 2020 19:34:51.185 # +monitor master myredis 127.0.0.1 6379 quorum 1
14533:X 21 Nov 2020 19:34:51.186 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:34:51.187 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379

测试主机崩掉之后

#哨兵监测到主机宕机
14533:X 21 Nov 2020 19:38:17.688 # +sdown master myredis 127.0.0.1 6379
#全部哨兵检测到主机宕机
14533:X 21 Nov 2020 19:38:17.688 # +odown master myredis 127.0.0.1 6379 #quorum 1/1
14533:X 21 Nov 2020 19:38:17.688 # +new-epoch 1
14533:X 21 Nov 2020 19:38:17.688 # +try-failover master myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.690 # +vote-for-leader 47af3a2c6bf1d6a87e28b65d169e39c5f9e3b3e6 1
14533:X 21 Nov 2020 19:38:17.690 # +elected-leader master myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.690 # +failover-state-select-slave master myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.761 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.761 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.833 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.836 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.836 # +failover-state-reconf-slaves master myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:17.885 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:18.886 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:18.887 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6379
14533:X 21 Nov 2020 19:38:18.962 # +failover-end master myredis 127.0.0.1 6379
#主机从6379切换到6380
14533:X 21 Nov 2020 19:38:18.962 # +switch-master myredis 127.0.0.1 6379 127.0.0.1 6380
14533:X 21 Nov 2020 19:38:18.963 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ myredis 127.0.0.1 6380
14533:X 21 Nov 2020 19:38:18.963 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380
14533:X 21 Nov 2020 19:38:48.992 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ myredis 127.0.0.1 6380

当主机宕机之后,投票选举一个从机成为主机,当主机再次启动,会自动变为从机

哨兵模式:
优点

1、哨兵集群基于主从复制,所有主从复制的优点他都由
2、主从可以自动切换,故障可以转移,系统的可用性会更好
3、哨兵模式就是主从复制的升级,从手动到自动,更加健壮

缺点

1、redis不好在线扩容,集群容量一旦到达上限,在线扩容就很麻烦
2、实现哨兵模式的配置属性繁多

哨兵模式全部配置:

#禁止保护模式
protected-mode no
#配置监听的主服务器,这里 sentinel monitor 代表监控
#mymaster代表服务器名称,可以自定义
#192.168.11.128代表监控的主服务器
#6379代表端口
#2代表只有两个或者两个以上的哨兵认为主服务器不可用的时候,才会做故障切换操作
sentinel monitor mymaster 192.168.11.128 6379 2
#sentinel auth-pass 定义服务的密码
#mymaster服务名称
#abcdefg Redis服务器密码
sentinel auth-pass mymaster abcdefg
#哨兵进程端口
port
#哨兵进程服务临时文件夹,默认为 /tmp,要保证有可写入的权限
dir /tmp
#指定哨兵在监测 Redis 服务时,当 Redis 服务在一个亳秒数内都无 法回答时,单个哨兵认为的主观下线时间,默认为 30000(30秒)
sentinel down-after-milliseconds
#指定可以有多少 Redis 服务同步新的主机,一般而言,这个数字越 小同步时间就越长,而越大,则对网络资源要求则越高
sentinel parallel-syncs 
#指定在故障切换允许的亳秒数,当超过这个亳秒数的时候,就认为 切换故障失败,默认为 3 分钟
sentinel failover-timeout  
#指定 sentinel 检测到该监控的 redis 实例指向的实例异常时,调用的 报警脚本。该配置项可选,比较常用
sentinel notification-script

缓存穿透和雪崩(面试高频)

缓存穿透和击穿

	缓存穿透:当用户去请求数据的时候,会先从缓存中寻找,如果没有,则会从数据库中寻找,如果有人恶意攻击,
绕过缓存,去大量的请求数据库服务器,就会对数据库造成很大的压力,导致宕机,这种现象成为`缓存穿透`
	缓存击穿:当缓存中的一条数据,被请求了很多次,这条数据设置的过期时间60秒,当数据过期还没有重新set的时候,
大量的用户请求就会绕过缓存,直接请求数据库服务器,也可能会造成数据库服务器的宕机,这种现象称为`缓存击穿`
例如:微博的热点(宕机)

解决方案:

1、设置热点key永不过期 (比较容易出问题,不可能永远不过期)
2、加互斥锁,当请求绕过缓存,请求数据库服务器的时候,设置线程锁,只允许一个线程去请求数据库,其他线程进行等待

缓存雪崩

当缓存中大量的key同时过期,或者redis宕机

产生雪崩的原因:
比如双十一零点抢购之前,这波商品会集中的放入到缓存中,假设缓存一个小时过期,那么一个小时之后,
这些商品的查询都会集中到数据库中,对于数据库而言会产生周期性的压力波动,出现宕机的风险

解决方案:

1、redis的高可用
多设置几台redis服务器
2、限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如某一个key只允许一个线程去查询数据,
写缓存,其他线程等待
3、数据预热
把可能的数据预先访问一遍,这样大部分的数据都会预设到缓存中,设置不同的过期时间,尽量均匀

视频地址

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值