Redis基础及原理详解

Redis基础及原理详解

前言:以下是最近学习redis的一些笔记总结,文中如有不当的地方欢迎批评指正,重在记录与学习,笔芯~~

Nosql概述

演进历史

  1. 单机mysql
    单机mysql

  2. Memcached(缓存)+Mysql+垂直拆分(读写分离)
    优化数据结构和索引->文件缓存(IO问题)->Memcached
    读写分离

  3. 分库分表+水平拆分+MYSQL集群
    表锁,影响效率,出现严重的锁问题
    -> innodb:行锁
    ->使用分库分表解决写压力
    ->mysql集群
    集群

  4. 当前,大数据时代
    3V, 问题描述(海量Volume,多样Variety,实时Velocity)
    3高,程序要求(高并发,高可扩,高性能)
    数据量大,变化快,数据结构复杂
    当前

传统RDBMS和NOSQL

传统RDBMS:

  • 结构化
  • sql
  • 数据与关系都存在单独的表中
  • 操作语言,数据定义语言
  • 严格一致性
  • 基础事务

Nosql:

  • 无固定查询语言
  • 键值对存储,列存储,文档存储,图形数据库(社交关系)
  • 最终一致性
  • CAP定理与BASE
  • 高性能,高可用,高可扩

NoSql的四大分类

  1. KV键值对
  • 新浪:Redis
  • 美团:Redis+Tair
  • 阿里、百度:Redis+memecache
  1. 文档型数据库(bsaon和json)
  • MongoDB
    基于分布式文件存储的数据库,c++,主要用来处理大量文档
    介于关系型和非关系型之间
  • CouchDB
  1. 列存储数据库
  • HBase
  • 分布式文件系统
  1. 图关系数据库

Redis(remote dictionary server,远程字典服务)

作用

  • 内存存储、持久化,内存中断电即失(rdb,aof)
  • 效率高,可用于高速缓存
  • 发布订阅
  • 地图信息分析
  • 计时器、计数器(浏览量)

特性

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

安装

  1. redis-benchmark(压力测试工具)
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
  1. redis服务
    参考博文

基础知识

  1. redis默认有16个数据库(0-15),默认使用0个
  • 可使用select进行切换
    select 3
  1. 基本命令
  • 查看所有key
    keys *
  • 清除当前数据库
    flushdb
  • 清除全部数据库内容
    flushall
  • 设置过期时间
    expire keyname seconds
  • 查看key过期时间
    ttl keyname
  1. redis是单线程的
    redis是基于内存操作的,c语言写的
    cpu->内存->硬盘

五大数据类型

  1. String
########################
127.0.0.1:6379[3]> keys * #查询所有key
1) "age"
2) "name"
127.0.0.1:6379[3]> get name #获得值
"sw"
127.0.0.1:6379[3]> exists name  #判断key是否存在
(integer) 1
127.0.0.1:6379[3]> append name huan #追加字符串
(integer) 6
127.0.0.1:6379[3]> get name
"swhuan"
127.0.0.1:6379[3]> strlen name #获取key的长度

##########################
(integer) 6
127.0.0.1:6379[3]> set views 0 #初始浏览量
OK
127.0.0.1:6379[3]> incr views # 自增1
(integer) 1
127.0.0.1:6379[3]> get views
"1"
127.0.0.1:6379[3]> incr views
(integer) 2
"2"
127.0.0.1:6379[3]> get views
"2"
127.0.0.1:6379[3]> decr views  # 自减1
(integer) 1
127.0.0.1:6379[3]> decr views
(integer) 0
127.0.0.1:6379[3]> incrby views 8 # 可设置步长,指定增量
(integer) 8
127.0.0.1:6379[3]> incrby views 10
(integer) 18
127.0.0.1:6379[3]> decrby views 3 # 可设置步长,指定减少量
(integer) 15
##########################
127.0.0.1:6379[3]> getrange name 2 6  字符串范围
"huan"

##########################
127.0.0.1:6379[3]> set key lallala
OK
127.0.0.1:6379[3]> setrange key 1 he  #替换指定位置开始的字符串
(integer) 7
127.0.0.1:6379[3]> get key
"lhelala"

###########################
# setex(set with expire) #设置过期时间
setex key1 30 
# setnx(set if not exits) # 不存在再设置(分布式锁中常使用)
setnx mykey "id"

###########################
#批量设置
#mset #同时设置多个key
#mget #同时获取多个值
msetnx k1 v1 k2 v2 #msetnx是一个原子操作,要么都成功,要么都失败
###########################
#对象
set user:1 {name:hh,age:18} #设置user:1对象的值为json字符串
###########################
getset #线get再set,如果不存在值,返回nil,如果存在值,获取原来的值并设置新的值
  1. list
################
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 2
1) "three"
2) "two"
3) "one"

127.0.0.1:6379> rpush list how #将值放在列表的尾部
(integer) 4
127.0.0.1:6379> lrange list  0 3 
1) "three"
2) "two"
3) "one"
4) "how"
##################
127.0.0.1:6379> lpop list #移除list第一个元素
"three"
127.0.0.1:6379> rpop list #移除list最后一个元素
"how"
127.0.0.1:6379> lrange list  0 -1
1) "two"
2) "one"
##################
127.0.0.1:6379> lindex list 0 #通过索引获取值
##################
127.0.0.1:6379> llen list #返回列表长度
(integer) 2
#################

127.0.0.1:6379> lrem list 1 one  # 移除指定的值,精确匹配
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
###################
127.0.0.1:6379> ltrim  list 1 2 #通过下标截取指定产犊,list已经改变了
OK
127.0.0.1:6379> lrange list 0 -1
1) "two"
###################
rpoplpush # 移除列表的最后一个元素,并添加新的元素
###################
lset #将列表中指定下标的值替换为另外的值,更新操作,不存在报错,存在更新
127.0.0.1:6379> lset  list 0 three
OK
127.0.0.1:6379> lrange list 0 -1
1) "three"
######################
#linsert # 将某个具体的值value插入到列表某个元素的前面或后面
127.0.0.1:6379> lrange list 0 -1
1) "three"
127.0.0.1:6379> linsert list before "three" "two"
(integer) 2
127.0.0.1:6379> linsert list after "two" "ceo"
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "ceo"
3) "three"
  1. set
127.0.0.1:6379> sadd myset "hello" #添加元素
(integer) 1
127.0.0.1:6379> sadd myset "hello2"
(integer) 1
127.0.0.1:6379> sadd myset "hello3"
(integer) 1
127.0.0.1:6379> smembers myset #查询set所有元素
1) "hello3"
2) "hello2"
3) "hello"
127.0.0.1:6379> sismember myset world  #判断某个值是否在set中
(integer) 0
127.0.0.1:6379> sismember myset hello
(integer) 1
###############
127.0.0.1:6379> scard myset # 获取set集合中的指定元素
(integer) 3
###############
127.0.0.1:6379>  srandmember myset 2 #随机抽取指定个数的元素
1) "hello2"
2) "hello3"
127.0.0.1:6379> srandmember myset #随机抽取一个元素
"hello"
127.0.0.1:6379> srandmember myset
"hello3"
################
#s删除指定的key,随机删除key
127.0.0.1:6379> spop myset
"hello3"

################
127.0.0.1:6379> sadd key1 a
(integer) 1
127.0.0.1:6379> sadd key1 b
(integer) 1
127.0.0.1:6379> sadd key1 c
(integer) 1
127.0.0.1:6379> sadd key2 d
(integer) 1
127.0.0.1:6379> sadd key2 c
(integer) 1
127.0.0.1:6379> sadd key2 e
(integer) 1
127.0.0.1:6379> sdiff key1 key2   #差集
1) "a"
2) "b"
127.0.0.1:6379> sinter key1 key2  #交集
1) "c"
127.0.0.1:6379> sunion key1 key2  #并集
1) "a"
2) "c"
3) "e"
4) "b"
5) "d"
  1. hash
#Map集合,key-map
OK
127.0.0.1:6379> hset myhash field1 swhuan # set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash field1 #获取一个值
"swhuan"
127.0.0.1:6379> hmset myhash field hello field2 swhuan #set多个key-value
OK
127.0.0.1:6379> hmget myhash field  field2  #同时多个值
1) "hello"
2) "swhuan"
127.0.0.1:6379> hgetall myhash  #获取全部数据
1) "field1"
2) "swhuan"
3) "field"
4) "hello"
5) "field2"
6) "swhuan"
##################
127.0.0.1:6379> hdel myhash field1  #删除指定key字段,对于value也消失
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field"
2) "hello"
3) "field2"
4) "swhuan"

###################
127.0.0.1:6379> hlen myhash #获取hash表的字段数量
(integer) 2


####################
127.0.0.1:6379> hexists myhash field #判断指定hash 是否存在
(integer) 1

####################
127.0.0.1:6379> hkeys myhash #只获取所有field
1) "field"
2) "field2"
127.0.0.1:6379> hvals myhash  #只获取所有value
1) "hello"
2) "swhuan"

###################
127.0.0.1:6379> hset myhash field3 5 
(integer) 1
127.0.0.1:6379> hincrby myhash field3 2 # 指定增量
(integer) 7
127.0.0.1:6379> 
127.0.0.1:6379> hsetnx myhash field4 hello  #如果不存在则设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field4 hello  #如果存在则不能设置
(integer) 0
#hash更适合对象的存储,string适合字符串存储
  1. zset(有序集合)
#在set基础上,增加一个值
#set k1 v1 zset k1 score1 v1
127.0.0.1:6379> zadd myzset 1 one #
(integer) 1
127.0.0.1:6379> zadd myzset 2 two 3 three
(integer) 2
127.0.0.1:6379> zrange myzset 0 -1
1) "one"
2) "two"
3) "three"

#########################
127.0.0.1:6379> zrangebyscore myzset -inf +inf #从小到大排序
1) "one"
2) "two"
3) "three"

127.0.0.1:6379> zrevrange myzset 0 -1  #从大到小排序
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> zrangebyscore myzset -inf +inf withscores #排序并附带成绩
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
#########################
127.0.0.1:6379> zrem myzset one #移除有序集合的指定元素
(integer) 1
127.0.0.1:6379> zrange myzset  0 -1
1) "two"
2) "three"
127.0.0.1:63
##########################
127.0.0.1:6379> zcard myzset #获取有序集合中的个数
(integer) 2

##########################
127.0.0.1:6379> zcount myset 1 2  #获取指定区间的成员数量
(integer) 0

三种特殊数据类型

  1. geospatial (地理位置)
##规则:两极无法直接添加,一般是下载城市数据,通过程序一次性导入
##参数:key 值(经度、纬度、名称)
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> geopos china:city beijing #获取指定城市的经度纬度
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"

############################
127.0.0.1:6379> geodist china:city beijing shanghai #查询北京到上海的直线距离
"1067378.7564"
127.0.0.1:6379> geodist china:city beijing shanghai km
"1067.3788"

#############################
#获取附近的人,通过半径距离
#以110 30,寻找方圆1000km内的城市
127.0.0.1:6379> georadius china:city 110 30 1000 km 
1) "chongqi"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist #显示到中间距离的位置
1) 1) "chongqi"
   2) "341.9374"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord #显示他人定位信息
1) 1) "chongqi"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord count 1 #筛选指定结果
1) 1) "chongqi"
   2) 1) "106.49999767541885376"
      2) "29.52999957900659211"
#############################
127.0.0.1:6379> georadiusbymember  china:city beijing 1000 km #找出指定元素周围其他元素
1) "beijing"

  1. hyperlolog(基数统计)
#基数(不重复元素的个数),可接受误差(0.81%错误率,可忽略)
127.0.0.1:6379> pfadd mykey1 a b c d e f g q #创建一组元素
(integer) 1
127.0.0.1:6379> pfadd mykey2 i p q h e f g
(integer) 1
127.0.0.1:6379> pfcount mykey1 #通国际元素中基数数量
(integer) 8
127.0.0.1:6379> pfcount mykey2
(integer) 7
127.0.0.1:6379> pfmerge mykey3 mykey1 mykey2 #合并两组到mykey3
OK
127.0.0.1:6379> pfcount mykey3
(integer) 11

  1. bitmaps(位图)
#位存储
#eg:统计疫情感染数,活跃,打卡
#一周打开天数
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 1
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 0
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
127.0.0.1:6379> getbit sign 3  #获取指定值状态
127.0.0.1:6379> bitcount sign #统计打卡天数
(integer) 4

redis事务

  1. 本质
    一组命令的集合,一个事务中所有命令会被序列化,按顺序执行
  • 一次性
  • 顺序性
  • 排他性
  1. 注意事项
  • redis事务没有隔离级别概念
  • redis单条命令保证原子性,但事务不保证原子性
  1. 步骤
    (i) 正常执行
  • 开启事务(multi)
  • 命令入队
  • 执行事务(exec)

(ii) 放弃事务

  • 开启事务(multi)
  • 命令入队
  • 取消事务(discard)
  1. 异常
  • 编译型异常(代码问题,命令错误)
    事务中所有命令都不会被执行
  • 运行时异常(1/0)
    如果事务队列中存在语法异常,执行命令时,其他命令可正常执行,错误命令抛出异常
  1. 监控(watch)
  • 悲观锁
    认为什么时候都会出问题,无论做什么都会加锁
  • 乐观锁
    认为什么时候都不会出问题,不会上锁,更新数据时判断此期间是否修改过该数据
    (i) 正常执行成功
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> exec

(ii) 多线程测试修改值,使用watch可当作redis乐观锁操作

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 #执行之前,另外一个线程修改了money值,此时会导致事务执行失败
(nil)

(iii) 如果修改失败,获取新值即可

127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> unwatch  #如果发现事务执行失败,就先解锁
OK
127.0.0.1:6379> watch money #获取新值,再次watch
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 1
QUEUED
127.0.0.1:6379> incrby out 1
QUEUED
127.0.0.1:6379> exec #比对事务期间,监视值是否发生变化,无变化则成功,否则失败
1) (integer) 99
2) (integer) 1

Jedis

  1. 引入jedis依赖包
    <dependencies>
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!--  fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
    </dependencies>

  1. 编码
    代码与原生命令一致
  • 连接数据库
  • 操作命令
  • 断开连接
public static void main(String[] args) {

        Jedis jedis = new Jedis("192.168.2.20",6379);

        try {
            System.out.println("测试是否成功连接:" + jedis.ping());
        } catch (Exception e) {
            System.out.println("连接redis服务器失败");
        } finally {
            if (null != jedis) {
                jedis.close();
            }
        }

    }
  1. 常见API
  • key相关操作
public static void main(String[] args) {

        Jedis jedis = new Jedis("192.168.2.20", 6379);

        System.out.println("清空数据:" + jedis.flushDB());

        System.out.println("判断key是否存在:" + jedis.exists("username"));

        System.out.println("新增键值对:" + jedis.set("username", "swhuan"));

        System.out.println("新增键值对:" + jedis.set("token", "toke_xxxx"));

        Set<String> keys = jedis.keys("*");

        System.out.println("查询所有key:" + keys);
        

        System.out.println("删除键token:" + jedis.del("token"));

        System.out.println("查看键username所存储的值的类型:" + jedis.type("username"));

        System.out.println("随机返回key空间的一个:" + jedis.randomKey());

        System.out.println("重命名key:" + jedis.rename("username", "name"));

        System.out.println("取出改后name的值:" + jedis.get("name"));

        System.out.println("按索引查询:" + jedis.select(0));

        System.out.println("删除所有数据库的key:" + jedis.flushAll());


    }
  • String
 public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.2.20", 6379);

        System.out.println("清空数据:" + jedis.flushDB());

        //添加单个数据
        System.out.println(jedis.set("k1","v1"));
        System.out.println(jedis.set("k2","v2"));
        System.out.println(jedis.set("k3","v3"));

        //删除
        System.out.println("删除键k2:"+jedis.del("k2"));
        System.out.println("获取键k2对应值:"+jedis.get("k2"));
        System.out.println("获取键k1对应值:"+jedis.get("k1"));

        //追加
        System.out.println("在k3后加入值:"+jedis.append("k3","end!!"));
        System.out.println("获取键k3对应的值:"+jedis.get("k3"));

        //同时添加多个键值对
        jedis.mset("k001","v001","k002","v002");

        //同时获取多个键值对
        System.out.println("获取多个键值对:"+jedis.mget("k001","k002"));

        //同时删除多个键值对
        jedis.del("k001","k001");

        System.out.println("清空数据:" + jedis.flushDB());

        //setnx:新增键值对防止覆盖原先值
        jedis.setnx("k1","v1");
        jedis.setnx("k2","v2");
        jedis.setnx("k2","new v2");
        System.out.println(jedis.get("k1"));
        System.out.println(jedis.get("k2"));

        //setex:新增键值对并设置有效时长
        jedis.setex("k3",2,"v3");
        System.out.println(jedis.get("k3"));
        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        //获取原值并更新新值
        System.out.println(jedis.getSet("k2","v0002"));

        //获取值的子串
        System.out.println("k2的子串:"+jedis.getrange("k2",0,2));
    }
  • List
public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.2.20", 6379);
        System.out.println("清空数据:" + jedis.flushDB());

        //添加list
        jedis.lpush("c","v1","v2","v3");
        jedis.lpush("c","v4");
        jedis.lpush("c","v5");
        jedis.lpush("c","v6");

        //获取list内容
        System.out.println("c的内容:"+jedis.lrange("c",0,-1));
        System.out.println("c的0-2区间内容:"+jedis.lrange("c",0,2));


        //删除列表指定值,第二个参数为删除的个数,后add的先删,类似于出栈
        System.out.println("删除指定元素个数:"+jedis.lrem("c",2,"v2"));
        System.out.println("c的全部内容:"+jedis.lrange("c",0,-1));
        System.out.println("删除下表0-3区间外的内容:"+jedis.ltrim("c",0,3));
        System.out.println("c的内容:"+jedis.lrange("c",0,-1));

        //出栈
        System.out.println("c出栈(左端):"+jedis.lpop("c"));
        System.out.println("c的内容:"+jedis.lrange("c",0,-1));
        System.out.println("c出栈(右端):"+jedis.rpop("c"));
        System.out.println("c的内容:"+jedis.lrange("c",0,-1));

        //修改
        System.out.println("修改指定下标1的内容:"+jedis.lset("c",1,"v000"));
        System.out.println("c的内容:"+jedis.lrange("c",0,-1));

        //排序
        jedis.lpush("sortedList","2","1","3","7","5","8");
        System.out.println("sortedList排序前:"+jedis.lrange("sortedList",0,-1));
        jedis.sort("sortedList");
        System.out.println("sortedList排序后:"+jedis.sort("sortedList"));
        
    }
  • Set
 public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.2.20", 6379);
        System.out.println("清空数据:" + jedis.flushDB());

        //向集合添加元素
        jedis.sadd("s","e1","e2","e3","e4","e5","e6");
        jedis.sadd("s","e7");

        //获取集合所有元素
        System.out.println("s集合所有元素:"+jedis.smembers("s"));

        //删除元素
        System.out.println("删除元素e1:"+jedis.srem("s","e1"));
        System.out.println("s集合所有元素:"+jedis.smembers("s"));

        //移除元素
        jedis.spop("s");
        System.out.println("s集合所有元素:"+jedis.smembers("s"));

        //获取元素总个数
        System.out.println("s集合所有元素个数:"+jedis.scard("s"));

        //判断元素是否存在于集合中
        System.out.println("判断e5是否在s集合中:"+jedis.sismember("s","e5"));

        //集合间删除操作
        jedis.sadd("s1","e3","e5","e7","e6","e8","e2","e1");
        jedis.sadd("s2","e3","e4","e6","e9","e2");
        System.out.println("将s1中删除e1并存入s2中:"+jedis.smove("s1","s2","e1"));
        System.out.println("将s1中删除e7并存入s2中:"+jedis.smove("s1","s2","e7"));
        System.out.println("s1集合所有元素:"+jedis.smembers("s1"));
        System.out.println("s2集合所有元素:"+jedis.smembers("s2"));

        //集合运算
        System.out.println("集合s1和s2的交集:"+jedis.sinter("s1","s2"));
        System.out.println("集合s1和s2的并集:"+jedis.sunion("s1","s2"));
        System.out.println("集合s1和s2的差集:"+jedis.sdiff("s1","s2"));
        System.out.println("s1和s2交集并将交集保存到s3:"+jedis.sinterstore("s3","s1","s2"));
        System.out.println("s3集合所有元素:"+jedis.smembers("s3"));

    }
  • Hash
  public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.2.20", 6379);
        System.out.println("清空数据:" + jedis.flushDB());


        //添加元素
        Map<String,String> map = new HashMap<String, String>();
        map.put("k1","v1");
        map.put("k2","v2");
        map.put("k3","v3");
        jedis.hset("hash",map);
        jedis.hset("hash","k4","v4");

       //获取元素
        System.out.println("散列hash所有键值对为:"+jedis.hgetAll("hash"));
        System.out.println("散列hash所有键为:"+jedis.hkeys("hash"));
        System.out.println("散列hash所有值为:"+jedis.hvals("hash"));
        System.out.println("散列hash键值对个数:"+jedis.hlen("hash"));
        System.out.println("删除一个或多个键值对:"+jedis.hdel("hash","k2","k1"));
        System.out.println("散列hash是否存在k2:"+jedis.hexists("hash","k2"));
        System.out.println("获取散列hash中k3的值:"+jedis.hmget("hash","k3"));


    }
  • Zset
 public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.2.20", 6379);
        System.out.println("清空数据:" + jedis.flushDB());

        //添加数据
        jedis.zadd("z1",3,"v1");
        jedis.zadd("z1",2,"v2");
        jedis.zadd("z1",5,"v3");
        jedis.zadd("z1",2,"v4");
        jedis.zadd("z1",1,"v5");

        //获取有序集合所有元素
        System.out.println("z1有序集合所有元素为:"+jedis.zrange("z1",0,-1));

        //获取有序集合
        System.out.println("z1从小到大排序:"+jedis.zrangeByScore("z1",1,6));
        System.out.println("z1从大到小排序:"+jedis.zrevrange("z1",0,-1));
    }
  • 事务相关
 public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.2.20", 6379);
        System.out.println("清空数据:" + jedis.flushDB());

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name","swhuan");
        jsonObject.put("age",23);
        String info = jsonObject.toJSONString();
        //开启事务
        Transaction multi = jedis.multi();
        try {
            multi.set("u1",info);
            multi.set("u2",info);
            //代码抛出异常,放弃事务
            int i= 1/0;
            //执行事务
            multi.exec();
        }catch (Exception e){
            //放弃事务
            multi.discard();
        }finally {
            System.out.println(jedis.get("u1"));
            System.out.println(jedis.get("u2"));
            //关闭连接
            jedis.close();
        }
    }

spring boot 整合redis

SpringBoot操作数据:sping-data 下 jpa jdbc mongodb redis用法类似
SpringData是和SpringBoot齐名的项目

说明:在SpringBoot2.x之后,原来使用的jedis被替换成了lettuce
jedis:采用直连,多线程不安全,为避免不安全,使用jedis pool连接池,BIO
lettuce:采用netty,实例可在多线程中共享,线程安全,可减少线程数量,NIO

  1. 源码分析
    SpringBoot所有配置类都有一个自动配置类,自动配置类都会绑定一个properties配置文件
    Redis配置类:RedisAutoConfiguration
    绑定的propertiess配置文件:RedisProperties
    @Bean
    //可自定义RedisTemplate替换这个默认bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        //默认的RedisTemplate,没有过多设置,redis对象需要序列化
        //泛型是<Object, 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) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
  1. 引入依赖
     <!--     redis-->
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
   </dependency>

     <!--    fastjson-->
   <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
  </dependency>
  1. 配置连接
spring.redis.host=192.168.2.20
spring.redis.port=6379

  1. 简单测试(使用默认redisTemplate)
@SpringBootTest
class Redis02SpringbootApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //opsForValue :操作字符串 类似于String
        //opsForList :操作list 类似list
        //opsForSet :操作set 类似set
        //opsForHash :操作hash 类似hash ...

       //获取redis连接
      //  RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
      //  connection.flushDb();
        redisTemplate.opsForValue().set("k1","v1");
        System.out.println(redisTemplate.opsForValue().get("k1"));


    }

}

  1. 自定义redisTemplate
    默认的redisTemplate序列化配置如图所示,在日常使用中我们往往会使用json序列化
    序列化
    序列化
@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(factory);

        //序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也用Strin序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        template.setConnectionFactory(factory);
        return template;
    }

}

  1. redis工具类
/**
 * redis工具类
 *
 * @author swhuan
 */
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public  boolean expire(final String key, final long timeout) {

        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {

        Boolean ret = redisTemplate.expire(key, timeout, unit);
        return ret != null && ret;
    }

    /**
     * 删除单个key
     *
     * @param key 键
     * @return true=删除成功;false=删除失败
     */
    public  boolean del(final String key) {

        Boolean ret = redisTemplate.delete(key);
        return ret != null && ret;
    }

    /**
     * 删除多个key
     *
     * @param keys 键集合
     * @return 成功删除的个数
     */
    public  long del(final Collection<String> keys) {

        Long ret = redisTemplate.delete(keys);
        return ret == null ? 0 : ret;
    }

    /**
     * 存入普通对象
     *
     * @param key Redis键
     * @param value 值
     */
    public void set(final String key, final Object value) {

        redisTemplate.opsForValue().set(key, value, 1, TimeUnit.MINUTES);
    }

    // 存储普通对象操作

    /**
     * 存入普通对象
     *
     * @param key 键
     * @param value 值
     * @param timeout 有效期,单位秒
     */
    public void set(final String key, final Object value, final long timeout) {

        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 获取普通对象
     *
     * @param key 键
     * @return 对象
     */
    public Object get(final String key) {

        return redisTemplate.opsForValue().get(key);
    }

    // 存储Hash操作

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public  void hPut(final String key, final String hKey, final Object value) {

        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 往Hash中存入多个数据
     *
     * @param key Redis键
     * @param values Hash键值对
     */
    public  void hPutAll(final String key, final Map<String, Object> values) {

        redisTemplate.opsForHash().putAll(key, values);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public  Object hGet(final String key, final String hKey) {

        return redisTemplate.opsForHash().get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public List<Object> hMultiGet(final String key, final Collection<Object> hKeys) {

        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    // 存储Set相关操作

    /**
     * 往Set中存入数据
     *
     * @param key Redis键
     * @param values 值
     * @return 存入的个数
     */
    public  long sSet(final String key, final Object... values) {
        Long count = redisTemplate.opsForSet().add(key, values);
        return count == null ? 0 : count;
    }

    /**
     * 删除Set中的数据
     *
     * @param key Redis键
     * @param values 值
     * @return 移除的个数
     */
    public  long sDel(final String key, final Object... values) {
        Long count = redisTemplate.opsForSet().remove(key, values);
        return count == null ? 0 : count;
    }

    // 存储List相关操作

    /**
     * 往List中存入数据
     *
     * @param key Redis键
     * @param value 数据
     * @return 存入的个数
     */
    public  long lPush(final String key, final Object value) {
        Long count = redisTemplate.opsForList().rightPush(key, value);
        return count == null ? 0 : count;
    }

    /**
     * 往List中存入多个数据
     *
     * @param key Redis键
     * @param values 多个数据
     * @return 存入的个数
     */
    public long lPushAll(final String key, final Collection<Object> values) {
        Long count = redisTemplate.opsForList().rightPushAll(key, values);
        return count == null ? 0 : count;
    }

    /**
     * 往List中存入多个数据
     *
     * @param key Redis键
     * @param values 多个数据
     * @return 存入的个数
     */
    public  long lPushAll(final String key, final Object... values) {
        Long count = redisTemplate.opsForList().rightPushAll(key, values);
        return count == null ? 0 : count;
    }

    /**
     * 从List中获取begin到end之间的元素
     *
     * @param key Redis键
     * @param start 开始位置
     * @param end 结束位置(start=0,end=-1表示获取全部元素)
     * @return List对象
     */
    public List<Object> lGet(final String key, final int start, final int end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

}

redis配置(redis.conf)详解

  • 网络NETWORK
bind 0.0.0.0 # 绑定的ip
protected-mode yes # 保护模式
port 6379 #端口设置
  • 通用GENERAL
daemonize yes #以守护进程方式运行,默认no,需自行打开
pidfile /var/run/redis_6379.pid #如果以后台方式运行,需指定pis文件

# 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 ""  #日志的文件名
databases 16  #默认的数据库数量
  • 快照NAPSHOTTING
    持久化,在规定的时间内,执行多少次操作,则会持久化到文件(rdb、aof),
    redis是内存数据库,如果没有持久化,数据断电即失
save 900 1 #如果900s内,如果至少有一个key进行修改,则进行持久化操作
save 300 10
save 60 10000

stop-writes-on-bgsave-error yes #如果出错,是否继续工作

rdbcompression yes #是否压缩rdb文件,需要消耗一些cpu资源

rdbchecksum yes #保存rdb文件时,进行错误检查校验

dir ./  #rdb文件保存的目录

  • 复制REPLICATION
    此处略,后面主从复制会详解

  • 安全SECURITY

requirepass 123456 # 默认没有密码,需要自己设置

  • 客户端CLIENTS限制
maxclients 10000 #能连接的最大客户端数量
maxmemory <bytes> #最大的内存容量
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方式持久化

appendfilename "appendonly.aof" #aof持久化文件名

# appendfsync always #每次修改同步,消耗性能
appendfsync everysec #每秒同步一次
# appendfsync no #不同步,效率最高

redis持久化

Redis是内存数据库,断电即失,所以Redis提供了持久化功能

  1. RDB(Redis DataBase)
    Redis持久化方式默认使用RDB,保存的文件是dump.rdb
    在配置文件的快照NAPSHOTTING模块进行配置
    说明:生产环境会对dump.rdb进行备份
    该图片摘自网络,侵删!!!
    rdb
    触发机制
  • save规则满足的情况下
  • 执行flushall命令
  • 退出redis

使用rdb文件恢复数据

  • 查询redis启动目录
config get dir
  • 将rdb文件放置redis启动目录,redis启动时会自动检查dump.rdb文件,恢复其中数据

优点:

  • 适合大规模数据恢复
  • 对数据完整性要求不高

缺点:

  • 需要一定时间间隔,如果redis宕机,最后异常修改数据没有了
  • fork进程的时候,会占用一定内存空间
  1. AOF(APPEND ONLY MODE)
    以日志形式记录每个写操作,只许追加文件不可改写文件,redis启动后会读该文件重新构建数据,
    保存的文件时appendonly.aof文件
    在配置文件APPEND ONLY MODE部分进行配置
    默认不开启aof,需手动配置
    aof

aof文件有错误,可使用工具修复aof文件
redis-check-aof --fix appendonly.aof

使用aof恢复数据
如果aof文件正常,重启即可恢复

优点:

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

缺点:

  • 相对于数据文件来说,aof远大于rdb,修复速度比rdb慢
  • aof运行效率比rdb慢,所有redis默认配置rdb

Redis订阅发布

pub/sub是一种消息通信方式:发送者(pub)发送消息,订阅者(sub)接受消息
Redis客户端可以订阅任意数量的频道

  1. 订阅/发布消息图
  • 消息发送者
  • 频道
  • 消息订阅者
    该图片摘自网络,侵删!!!
    消息订阅
  1. 相关命令:
  • PSUBSCRIBE:订阅一个或多个符合给定模式的频道
  • PUBLISH:查看订阅与发布系统状态
  • PUBSUB:将信息发送到指定的频道
  • PUNSUBSCRIBE:退订所有给定模式的频道
  • SUBSCRIBE:订阅给定的一个或多个频道的信息
  • UNSUBSCRIBE:指退订给定的频道
  1. 测试
    发送端:
127.0.0.1:6379> PUBLISH sun "today" #消息者发布一个频道
(integer) 1
127.0.0.1:6379> PUBLISH sun "today,message"
(integer) 1

订阅端:

127.0.0.1:6379> SUBSCRIBE sun #订阅频道sun
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "sun"
3) (integer) 1
#等待读取推送的信息
1) "message" #消息
2) "sun"  #频道
3) "today" #消息具体内容
1) "message"
2) "sun"
3) "today,message"

使用场景:

  • 实时消息系统
  • 实时聊天(频道当作聊天室,消息回显给所有人)
  • 订阅/关注系统
    说明:稍微复杂的场景,推荐使用消息中间件MQ

Redis集群搭建

环境配置

  1. 说明
  • 默认情况下,每台Redis服务器都是主节点
  • 只需配置从库,无需配置主库
  1. 步骤
    此处搭建示例是伪集群
  • 复制3个配置文件
  • 修改配置文件对应信息(端口、pid文件名、log文件名、dump.log文件名)
  • 指定配置文件,分别启动3个redis服务
redis-server redis79.conf
redis-server redis80.conf
redis-server redis81.conf 

#查看已启动的redis服务进程
ps -ef|grep redis
root       1696      1  0 12:35 ?        00:00:00 redis-server 0.0.0.0:6380
root       1703      1  0 12:36 ?        00:00:00 redis-server 0.0.0.0:6381
root       1713      1  0 12:36 ?        00:00:00 redis-server 0.0.0.0:6379

  • 在从机中进行配置
    认老大,此处一主(79)二从(80,81)
    方式一: 使用SLAVEOF命令
#在80从机上配置
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379

#在81从机上配置
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379

#在79主机上查看
127.0.0.1:6379> info replication
# Replication
role:master #角色
connected_slaves:2 #从节点个数
slave0:ip=127.0.0.1,port=6380,state=online,offset=20552,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=20552,lag=0
master_replid:c2e8da0a55eb076bff830832aeb189a435361f6f
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:20552
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:20552

方式二:配置从机配置文件复制(REPLICATION)模块

#在从机配置中添加如下配置

replicaof <masterip> <masterport> #设置主机ip和端口

masterauth <master-password> #如果主机设置了密码,需配置主机密码

主从切换

如果主机断开连接可使用SLAVEOF no one使自己成为主机,其他从节点重新认老大,如果此时原主机修复,那就重连

主从复制原理

概述

  • 将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave)
  • master以写为主,slave以读为主
  • 数据的复制是单向的,只能由主节点到从节点
  • 默认情况下,每台Redis服务器都是主节点
  • 一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点

作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量
  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础

复制原理

  • 全量复制
    一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份
  • 增量复制
    指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程
    增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令

哨兵(Sentinel)模式

自动选举老大的模式

概述

  • 哨兵是一个独立的进程
  • 哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例
  • 故障转移后,若原主机恢复,只能当作从机

单哨兵模式

  • 哨兵通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机
    该图片摘自网络,侵删!!!
    单哨兵模式

多哨兵模式

  • 主观下线:单哨兵认为主服务器不可用
  • 客观下线:认为主服务器不可用的哨兵数达到一定数量,进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机的过程

该图片摘自网络,侵删!!!
多哨兵模式

演示过程

  1. 配置哨兵配置文件sentinel.conf
#sentinel monitor 被监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1

说明:数字1代表主机挂了,让slave投票选出新主机

哨兵模式测试效果图

优点

  • 哨兵集群基于主从模式,具备主从模式所有优点
  • 主从可切换,故障可转移,高可用
  • 是主从模式的升级,从手动变为自动,更健壮

缺点

  • 不好扩容
  • 实现哨兵模式配置繁琐
  1. 启动哨兵
    redis-sentinel sentinel.conf

Redis常见问题及解决方案

缓存穿透

  1. 概念
    缓存未命中,于是向持久层数据库中查询,发现数据库中也没有,本次查询失败,当访问量大时,缓存都未命中,全都去请求持久层数据库,造成数据库压力过大的现象

  2. 解决方案

  • 布隆过滤器
    对所有可能查询参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免底层存储系统的查询压力

  • 缓存空对象
    该方式存在的两个问题:
    如果空值能被缓存,意味着缓存需要更多空间存储更多的键
    即使对空值设置了过期时间,还是会存在缓存层和存储层的数据时间窗口不一致,会影响需要保持一致性的业务

缓存击穿

  1. 概念
    一个key非常热点,高并发集中访问该点,在key失效瞬间,持续高并发请求穿透缓存直接请求底层数据库

  2. 解决方案

  • 设置热点数据永不过期
  • 加互斥锁
    分布式锁,保证每个key同时只要一个线程去查询后端服务,其他没有获得分布式锁的线程,等待即可

缓存雪崩

  1. 概念
    在某一时间段,缓存集中过期失效或者redis宕机

  2. 解决方案

  • 提供redis高可用
    搭建集群,增设redis服务器,异地多活

  • 限流降级

是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量

  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redis分布式锁的实现原理是基于Redis的SETNX指令和过期时间实现的。具体实现步骤是:当某个客户端请求获取锁时,该客户端向Redis服务器发送SETNX命令,若SETNX命令返回值为1,则表示获取锁成功,该客户端接着设置一个过期时间,用以避免锁一直持有;若SETNX命令返回值为0,则表示获取锁失败,此时客户端需要等待一段时间后重新尝试获取锁。 ### 回答2: Redis分布式锁是一种在分布式环境下协调多个进程或线程之间的互斥访问资源的机制。其原理是利用Redis提供的原子操作,通过在Redis中设置一个特定的键值对来实现锁的获取和释放。 具体实现步骤如下: 1. 获取锁:客户端通过执行SETNX命令(SET if Not eXists)来尝试在Redis中设置一个指定的键,并为其设置一个过期时间。如果命令成功执行并返回1,表示获取到了锁,否则表示锁已被其他客户端占用。 2. 使用锁:获取到锁之后,执行需要互斥访问资源的操作。 3. 释放锁:操作完成后,客户端通过执行DEL命令来删除所创建的键,释放锁。 需要注意的是,为了防止锁过期时间过长导致锁永远不会释放,可以使用SET命令来给锁设置一个过期时间,保证即使发生异常情况,锁也会在一段时间后自动释放。 此外,还需要考虑到锁的重入性和死锁的情况。对于锁的重入性,可以在Redis中维护一个计数器,记录获取锁的次数,在释放锁时将计数器减1,直到计数器为0时才真正释放锁。对于死锁的情况,可以为锁设置一个超时时间,如果获取锁的客户端在规定的时间内没有释放锁,则认为发生了死锁,其他客户端可以尝试获取该锁。 总之,Redis分布式锁通过利用Redis的原子操作和过期时间机制,可以实现多个进程或线程之间的资源互斥访问,确保系统在分布式环境下的稳定运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值