Redis数据库

GA666666 2021-11-28 PM 883℃ 1条

Redis入门

[TOC]

概述

Redis是什么?

Redis ( Remote Dictionary Server),即远程字典服务!
是一个开源的使用ANSI语言编写、支持网络、可基于内存亦可持久化的日志型、 Key-value数据産,并提供多种语言的API。

免费和开源!是当下最热门的 NOSQL技术之ー!也被人们称之为结构化数据库!

Redis能干嘛?
  1. 内存存储,持久化,内存中是断电即失,所以说持久化很重要
  2. 效率高,可以用于高速缓存
  3. 发布订阅系统
  4. 地图信息分析
  5. 计时器,计数器(浏览量)
  6. .....
特性
  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务
  5. .......
学习中的资料
  1. 官网:https://redis.io
  2. 中文网:https://www.redis.cn
  3. 下载地址:官网下载

Linux安装

  1. 下载安装包,上传到服务器
  2. 解压安装包

  3. 解压后进入redis文件夹,可以看到redis的配置文件 redis.config
  4. 基本环境安装

    yum install gcc-c++
    
    make
  5. redis默认安装目录
  6. mkdir gconfig
    
    cp /opt/redis-6.2.6/redis.conf gconfig
    
  7. redis默认不是后台启动,修改配置文件 daemonize 改为yes

  8. 启动redis服务

    redis-server gconfig/redis.conf 
  9. 连接服务

    redis-cli -p 6379
  10. 查看信息

    ps -ef|grep redis

    性能测试

    redis- benchmark是一个压力测试工具

    我们来简单测试一下

    #测试100个并放连接 100000请求

redis-benchmark -h localhost -p 6379 -c 100 -n 100000


## 基础知识

redis 默认有16个数据库

[root@VM-16-6-centos ~]# redis-cli -p 6379
127.0.0.1:6379> select 3 #切换数据库
OK
127.0.0.1:6379[3]> dbsize #查看DB大小
(integer) 0
127.0.0.1:6379[3]> set name "gaoxu"
OK
127.0.0.1:6379[3]> select 7
OK
127.0.0.1:6379[7]> dbsize
(integer) 0
127.0.0.1:6379[7]> select 3
OK
127.0.0.1:6379[3]> get name
"gaoxu"
127.0.0.1:6379[3]>


获取所有的key

127.0.0.1:6379[3]> keys * #keys
1) "name"


清空当前数据库

127.0.0.1:6379[3]> flushdb
OK
127.0.0.1:6379[3]> keys *
(empty list or set)
127.0.0.1:6379[3]>


清空所有数据库内容 **flushall**

> Redis 是单线程的 

## 五大数据类型

<img src="https://gitee.com/gao666666/images/raw/master/images/20211127213941.png" style="zoom:67%;" />

全文翻译:

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作**数据库**、**缓存**和**消息中间件MQ**。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。







### 常用命令

判断key是否存在

exists name


移除key

move name 1


给key设置过期时间 单位秒

expire name 10


查看key的剩余时间 ,返回秒

ttl name


查看类型

type name


### String(字符串)

127.0.0.1:6379> set key1 v1 #设置值
OK
127.0.0.1:6379> get key1 #获取值
"v1"
127.0.0.1:6379> EXISTS key1 #判断一个值是否存在
(integer) 1
127.0.0.1:6379> APPEND key1 hello #追加字符串,如果key不存在就相当于set
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> STRLEN key1 #获取字符串的长度
(integer) 7

127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> INCR views #自增1
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> DECR views #自增1
(integer) 1
127.0.0.1:6379> INCRBY views 10 #可以设置步长,指定增量
(integer) 11
127.0.0.1:6379> INCRBY views 10
(integer) 21
127.0.0.1:6379> DECRBY views 5 #可以设置步长,指定减量
(integer) 16

字符串范围
127.0.0.1:6379> set key1 "hello,gaoxu" #设置key1的值
OK
127.0.0.1:6379> get key1
"hello,gaoxu"
127.0.0.1:6379> GETRANGE key1 1 5 #截取字符串[1,5]
"ello,"
127.0.0.1:6379> GETRANGE key1 1 -1 #获取[1,end]
"ello,gaoxu"
127.0.0.1:6379> GETRANGE key1 0 -1 #获取全部字符串
"hello,gaoxu"

替换

127.0.0.1:6379> set key2 abcdefg #设置字符串
OK
127.0.0.1:6379> SETRANGE key2 1 xx #替换指定位置开始的字符串
(integer) 7
127.0.0.1:6379> get key2
"axxdefg"

setex (set with expire) #设置过期时间

setnx (set if not expire ) #不存设置(在分布式锁中会常常使用!)

127.0.0.1:6379> setex key3 60 hello #设置key3的过期时间和值
OK
127.0.0.1:6379> ttl key3
(integer) 54
127.0.0.1:6379> ttl key3
(integer) 53
127.0.0.1:6379> setnx key4 redis #给 key4 设置一个值,如果这个值不存在那么返回1
(integer) 1
127.0.0.1:6379> setnx key4 mongdb #给 key4 设置一个值,如果这个值存在那么返回0
(integer) 0
127.0.0.1:6379>

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #批量设置值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
3) "k2"
127.0.0.1:6379> mget k1 k2 k3 #批量获取
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #批量设置,如果其中一个设置失败,全部失败
(integer) 0
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
3) "k2"

对象

set user:1 tname: zhangsan,age:3}#设置一个user:1对象值为json字符来保存一个对象!

这里的key是一个巧妙的设计:user:fid}:{fied},如此设证在 Redis中是完全OK了

127.0.0.1: 6379> mset user:1: name zhangsan user:1: age 2
OK
127.0.0.1: 6379> mget user:1: name user:1: age1 zhangsan

getset #先get再set

127.0.0.1:6379> getset db redis #如果不存在值,则返回ni1
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongdb #如果存在值,则返回当前值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongdb"


### List (列表)

所有list操作都以L开头

127.0.0.1:6379> LPUSH list "one" #将一个或多个值擦汗人到列表头部
(integer) 1
127.0.0.1:6379> LPUSH list "two" "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 #将一个或多个值擦汗人到列表尾部
1) "three"
2) "two"
127.0.0.1:6379> RPUSH list first #将一个或多个值擦汗人到列表尾部
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "first"
127.0.0.1:6379>

POP
127.0.0.1:6379> LPOP list
"three"
127.0.0.1:6379> LRANGE list 0 -1 #移除列表的第一个元素
1) "two"
2) "one"
3) "first"
127.0.0.1:6379> RPOP list #移除列表的最后个元素
"first"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LINDEX list 0 #通过下标,获取某个元素
"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> LREM list 1 one #移除list集合中指定个数的value
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> LREM list one
(error) ERR wrong number of arguments for 'lrem' command
127.0.0.1:6379> LPUSH list three three
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> LREM list 2 three
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"

trim 修剪

127.0.0.1:6379> RPUSH list hello1 hello2 hello3
(integer) 3
127.0.0.1:6379> LTRIM list 0 1 #截取指定的长度
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "hello1"
2) "hello2"

rpoplpush:移除列表最后一个元素,并将它添加到另一个列表

127.0.0.1:6379> RPUSH list hello1 hello2 hello3
(integer) 3
127.0.0.1:6379> RPOPLPUSH list newlist
"hello3"
127.0.0.1:6379> LRANGE newlist 0 -1
1) "hello3"

lset 设置列表指定位置的值

127.0.0.1:6379> LRANGE newlist 0 -1
1) "hello3"
127.0.0.1:6379> lset list1 0 v1 #如果不存在列表就会报错
(error) ERR no such key
127.0.0.1:6379> lset newlist 0 v1
OK
127.0.0.1:6379> LRANGE newlist 0 -1
1) "v1"
127.0.0.1:6379> lset newlist 1 v2 #如果不存在当前下标就会报错
(error) ERR index out of range

linsert 向指定位置插入值

127.0.0.1:6379> lpush list v1 v2 v3
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "v3"
2) "v2"
3) "v1"
127.0.0.1:6379> LINSERT list before v2 ins
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "v3"
2) "ins"
3) "v2"
4) "v1"
127.0.0.1:6379> LINSERT list after v2 afins
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "v3"
2) "ins"
3) "v2"
4) "afins"
5) "v1"


> 小结

- 他实际上是一个链表,before Node after ,left right 都可以进行插入
- 如果key不存在,会创建新的 链表
- 如果key存在,会新增内容
- 移除所有的值,就是空链表,也代表不存在
- 在两边插入或改动值,效率最高
- 消息队列-》Lpush Rpop,栈,Lpush Lpop 



### Set(集合)

set中的值时不能重复的

sadd
127.0.0.1:6379> sadd myset "hello" "gaoxu" "woaini" #向一个集合中添加一个或多个成员
(integer) 3
127.0.0.1:6379> SMEMBERS myset #查看一个集合中的所有成员
1) "hello"
2) "woaini"
3) "gaoxu"
127.0.0.1:6379> SISMEMBER myset love #查看集合中是否包含某一元素
(integer) 0
127.0.0.1:6379> SISMEMBER myset gaoxu
(integer) 1

scard #获取set中所有元素的个数

127.0.0.1:6379> SCARD myset
(integer) 3
127.0.0.1:6379> SADD myset gaoxu
(integer) 0
127.0.0.1:6379> SADD myset ga666666
(integer) 1
127.0.0.1:6379> SCARD myset
(integer) 4

srem #移除set中的特定值

127.0.0.1:6379> SREM myset 666
(integer) 0
127.0.0.1:6379> SREM myset gaoxu
(integer) 1
127.0.0.1:6379> SCARD myset
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "ga666666"
2) "hello"
3) "woaini"

Srandmember #随机返回一个或多个值

127.0.0.1:6379> SRANDMEMBER myset
"woaini"
127.0.0.1:6379> SRANDMEMBER myset
"woaini"
127.0.0.1:6379> SRANDMEMBER myset
"hello"
127.0.0.1:6379> SRANDMEMBER myset 2
1) "hello"
2) "woaini"

spop #随机移除一个值

127.0.0.1:6379> SPOP myset
"woaini"
127.0.0.1:6379> SPOP myset
"hello"
127.0.0.1:6379> SMEMBERS myset
1) "ga666666"

smove #删除集合中指定的值,并存到另一个元素

127.0.0.1:6379> sadd myset hello world gaoxu
(integer) 3
127.0.0.1:6379> sadd myset2 set2
(integer) 1
127.0.0.1:6379> SMEMBERS myset
1) "hello"
2) "world"
3) "gaoxu"
127.0.0.1:6379> SMEMBERS myset2
1) "set2"
127.0.0.1:6379>
127.0.0.1:6379> SMOVE myset myset2 hello
(integer) 1
127.0.0.1:6379> SMEMBERS myset2
1) "hello"
2) "set2"

Sdiff/sinter #差集、交集、并集
127.0.0.1:6379> sadd s1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd s2 4 5 6 7 8
(integer) 5
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> SDIFF s1 s2 #差集
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> SINTER s1 s2 #交集
1) "4"
2) "5"
127.0.0.1:6379> SUNION s1 s2 #并集
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"


微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!

共同关注,共同爱好,二度好友(六度分割理论)

### Hash(哈希)

Map集合,key-Map集合,这时候值时一个map集合


127.0.0.1:6379> hset myhash field1 gaoxu #set 一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash field1
"gaoxu"
127.0.0.1:6379> HMSET myhash field1 ga666666 field2 libai #set 多个具体的key-value
OK
127.0.0.1:6379> HMGET myhash field1 field2 #get 多个具体的key-value
1) "ga666666"
2) "libai"
127.0.0.1:6379> HGETALL myhash #get 所有的key-value
1) "field1"
2) "ga666666"
3) "field2"
4) "libai"

hdel # 删除一个指定的字段
127.0.0.1:6379> hdel myhash field1
(integer) 1
127.0.0.1:6379> HGETALL myhash
1) "field2"
2) "libai"

hlen #获取hash表的自读但数量
127.0.0.1:6379> HLEN myset
(integer) 0
127.0.0.1:6379> hmset myset field1 666
OK
127.0.0.1:6379> HLEN myset
(integer) 1

hexists #判断是否存在某个值

127.0.0.1:6379> HEXISTS myhash field1
(integer) 0
127.0.0.1:6379> HEXISTS myhash field2
(integer) 1

hkeys #只获取所有的key
hvals #只获取所有的值

127.0.0.1:6379> HKEYS myhash
1) "field2"
127.0.0.1:6379> HVALS myhash
1) "libai"

incr decr #设置增长

127.0.0.1:6379> hset myhash field3 1
(integer) 1
127.0.0.1:6379> HINCRBY myhash field3 2
(integer) 3
127.0.0.1:6379> HINCRBY myhash field3 -1
(integer) 2

hsetnx #如果不存在字段设置一个值

127.0.0.1:6379> hsetnx myset field4 hello
(integer) 1
127.0.0.1:6379> hsetnx myset field4 world
(integer) 0


hash变更的数据 user name age尤其是是用户信息之类的,经常变动的信息 

### Zset(有序集合)

在set的基础上,增加了ー个值,zset k1 score1 v1

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
(error) ERR wrong number of arguments for 'zrange' command
127.0.0.1:6379> ZRANGE myset 0 -1
1) "one"
2) "two"
3) "three"

rangebyscore #按照score排序

127.0.0.1:6379> zadd salary 2000 wangyiming
(integer) 1
127.0.0.1:6379> zadd salary 500 gaoxu
(integer) 1
127.0.0.1:6379> zadd salary 7999 libai
(integer) 1
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf #从小到大排序
1) "gaoxu"
2) "wangyiming"
3) "libai"
127.0.0.1:6379> ZREVRANGE salary 0 -1 #从大到小排序
1) "libai"
2) "wangyiming"
127.0.0.1:6379> ZRANGEBYSCORE salary -inf +inf withscores #带分数排序
1) "gaoxu"
2) "500"
3) "wangyiming"
4) "2000"
5) "libai"
6) "7999"

zrem #移除
127.0.0.1:6379> ZREM salary gaoxu
(integer) 1
127.0.0.1:6379> ZRANGE salary 0 -1
1) "wangyiming"
2) "libai"

zcard #获取总数
127.0.0.1:6379> ZCARD salary
(integer) 2

zcount #获取区间内的数量

127.0.0.1:6379> zadd myset 1 aa
(integer) 1
127.0.0.1:6379> zadd myset 2 bb 3 cc
(integer) 2
127.0.0.1:6379> ZCOUNT myset 1 3
(integer) 3
127.0.0.1:6379> ZCOUNT myset 1 2
(integer) 2


## 三种特殊数据类型

### geospatial(地理位置)

朋友的定位,附近的人,打车距离计算?
Redis的Geo
可以查询一些测试数据:http://www.jsons.cn/lngcode/

GEOADD
GEODIST
GEOHASH
GEOPOS
GEORADIUS
GEORADIUSBYMEMBER
GEOSEARCH
GEOSEARCHSTORE


> geoadd

geoadd

规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入!

参数 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 chongqing 114.05 22.52 shenzheng 120.16 30.24 hangzhou
(integer) 3


> geopos

geopos #获取指定的经纬度

127.0.0.1:6379> geopos china:city beijing
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city chongqing
1) 1) "106.49999767541885376"
2) "29.52999957900659211"


> geodist

单位:

- **m** 表示单位为米。
- **km** 表示单位为千米。
- **mi** 表示单位为英里。
- **ft** 表示单位为英尺。

geodist #获取两点之间的距离

127.0.0.1:6379> GEODIST china:city shanghai beijing km
"1067.3788"
127.0.0.1:6379> GEODIST china:city shanghai beijing m
"1067378.7564"


> georadius

- **m** 表示单位为米。
- **km** 表示单位为千米。
- **mi** 表示单位为英里。
- **ft** 表示单位为英尺。

georadius #以一个坐标为中心 返回半径内的元素
127.0.0.1:6379> GEORADIUS china:city 116.40 39.8 100 km
1) "beijing"
127.0.0.1:6379> GEORADIUS china:city 116.40 39.8 10000 km
1) "chongqing"
2) "shenzheng"
3) "hangzhou"
4) "shanghai"
5) "beijing"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withdist
1) 1) "chongqing"
2) "341.9374"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km withcoord
1) 1) "chongqing"
2) 1) "106.49999767541885376"

  2) "29.52999957900659211"

> georadiusbymember

这个命令和 [GEORADIUS](http://www.redis.cn/commands/georadius.html) 命令一样, 都可以找出位于指定范围内的元素, 但是 `GEORADIUSBYMEMBER` 的中心点是由给定的位置元素决定的, 而不是像 [GEORADIUS](http://www.redis.cn/commands/georadius.html) 那样, 使用输入的经度和纬度来决定中心点指定成员的位置被用作查询的中心

> geohash

该命令将返回11个字符的Geohash字符串,所以没有精度Geohash,损失相比,使用内部52位表示

127.0.0.1:6379> GEOHASH china:city beijing chongqing
1) "wx4fbxxfke0"
2) "wm5xzrybty0"




### Hyperloglog(基数)

Redis2.8.9版本就更新了 Hyperloglog数据结构!
Redis Hyperloglog基数统计的算法!
优点:占用的内存是固定,264不同的元素的技术,只需要废12KB内存!如果要从内存角度来比较的话 Hyperloglog首选
网页的UV(一个人访向一个网站多次,但是还是算作一个人!)
传统的方式,Set保存用户的id,然后就可以统计set中的元素数量作为标准判断!
这个方式如果保存大量的用户id,就会比较麻烦!我们的目的是为了计数,而不是保存用户id
0.81%错误率!统计UV任务,可以忽略不计的

127.0.0.1:6379> pfadd myset a b c d e f g
(integer) 1
127.0.0.1:6379> pfadd myset2 g h i c a h i
(integer) 1
127.0.0.1:6379> PFCOUNT myset2
(integer) 5
127.0.0.1:6379> PFMERGE myset3 myset myset2
OK
127.0.0.1:6379> PFCOUNT myset3
(integer) 9


如果允许容错,那么一定可以使用 Hyperloglog!

如果不允许容错,就使用set或者自己的数据类型即可!

### BitMaps

> 位图

统计用户信息,活跃,不活跃!登录、未登录!打卡,365打卡!两个状态的,都可以使用 Bitmaps
Bitmaps位图,数据结构!都是操作二进制位来进行记录,就只有0和1两个状态!365天=365bit1字节=8bit46个字节左右!

> 测试 记录一周 的打卡情况

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 0
(integer) 0
127.0.0.1:6379> SETBIT sign 4 1
(integer) 0
127.0.0.1:6379> SETBIT sign 5 1
(integer) 0
127.0.0.1:6379> SETBIT sign 6 0
(integer) 0
127.0.0.1:6379> SETBIT sign 7 1
(integer) 0
127.0.0.1:6379> GETBIT sign 5
(integer) 1
127.0.0.1:6379> BITCOUNT sign 0 5
(integer) 4


## 事务

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

-------队列 set set set 执行-------


Redis没有隔离级别的概念
Redis单条命令式保存原子性的,但是事务不保证原子性!
redis事务

- 开启事务(multi)
- 命令入队(...)
- 执行事务(exec)

> 正常执行事务

127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> set k1 v1 #命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec #执行事务
1) OK
2) OK
3) OK


> 放弃事务

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k4
(nil)


> 编译型异常(代码有问题!命令有错!),事务中所有的命令都不会被执行!

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> setget
(error) ERR unknown command setget, with args beginning with:
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.


> 运行时异常(1/0),如果事务队列中存在语法性,那么执行命令的时候,其他命令式可以正常执行的

127.0.0.1:6379> set k1 gaoxu
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) OK


## Redis乐观锁

> 监视 watch

乐观锁

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

> redis的监视测试

正常执行

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20


多线程->开启watch,事务未执行前,另一个线程修改了money的值,导致事务失败

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
(nil)


<img src="https://gitee.com/gao666666/images/raw/master/images/20211128030817.png" style="zoom:67%;" />





## Jedis

我们要使用java 操作 Redis

> 什么是 **Jedis**      是 Redis官方推荐的java连接开发工具!使用java操作 Redis t中间件!如果你要使用java操作 redis,那么一定要对 Jedis十分的熟悉!

> 测试

1. 导入Jedis依赖
       <dependency>
           <groupId>redis.clients</groupId>
           <artifactId>jedis</artifactId>
           <version>3.3.0</version>
       </dependency>
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>fastjson</artifactId>
           <version>1.2.75</version>
       </dependency>

2. 编码测试

- 连接数据库
- 操作命令
- 断开连接 

### 常用API

String

list

hash

set

zset  

#### 事务

public class TestTx {

public static void main(String[] args) {
    Jedis jedis = new Jedis("82.156.129.111",6379);
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("username", "gaoxu");
    jsonObject.put("password", "147896");
    String result = jsonObject.toJSONString();
    jedis.flushDB();
    Transaction multi = jedis.multi();
    
    try {
        multi.set("user1", result);
        multi.set("user2", result);
        int i = 1/0;
        multi.set("user3", result);
        multi.exec();   //执行事务
    } catch (Exception e) {
        multi.discard();    //取消事务
        e.printStackTrace();
    } finally {
        System.out.println(jedis.get("user1"));
        System.out.println(jedis.get("user2"));
        jedis.close();      //关闭连接
    }
}

}


## SpringBoot整合

Springboot操作数据: spring- data jpa jdbc mongodb redis
Springdata也是和 Spring Boot齐名的项目!
说明:在 Springboot2X之后,原来使用的edis被替换为了 lettuce?

jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool连接池!BlO

lettuce:采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况,可以减少线程数量

1. 导入依赖
        <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-redis</artifactId>
           <version>2.5.6</version>
       </dependency>

2. 配置连接

spring.redis.host=82.156.129.111
spring.redis.port=6379




## Redis配置详解

启动的时候通过配置文件启动

> 网络

bind 127.0.0.1 #绑定ip
protected-mode yes #受保护模式
port 6379 #端口号


> 通用

daemonize yes #以守护进程的方式运行,默认是no,我们需要自己开启为yes! 也就是后台启动
pidfile/var/run/ redis-6379.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


> 快照

持久化,在规定的时间内,执行了多少次操作,则会持久化到文件rdb.aop

save 900 1 #如果900秒内,至少有一个key进行修改,那么就进行持久化操作
save 300 10 #如果300秒内,至少有10个key进行修改,那么就进行持久化操作
save 60 10000 #如果60 秒内,至少有100000个key进行修改,那么就进行持久化操作

stop-writes-on-bgsave-error yes #如果持久化出错是否继续工作
rdbcompression yes #是否压缩rdb文件
rdbchecksum yes #保存rdb文件时,是否进行校验
dir /www/server/redis/ #rdb文件按保存目录


> REPLICATION主从复制

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128154151.png" style="zoom:50%;" />


replicaof #配置主机的地址和端口号
masterauth #主机的密码






>SECURITY 

可以配置redis的密码
也可以通过命令配置
进入命令行
config set requirepass “123456” #设置密码
设置好后要验证密码才能使用命令行
auth 123456 #验证密码


> 限制CLIENT

maxclients 10000 #设置能连上redis的最大客户端数量
maxmemory #设置redis最大内存
maxmemory-policy noeviction #内存到达上限之后的处理策略,一共6中

1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
2、allkeys-lru : 删除lru算法的key   
3、volatile-random:随机删除即将过期key   
4、allkeys-random:随机删除   
5、volatile-ttl : 删除即将过期的  
6、noeviction : 永不过期,返回错误




> APPEND ONLY MODE   aof模式

appendonly no #默认关闭aof模式
appendfilename "appendonly.aof" #持久化文件的名字

appendfsync always #每次修改都会sync,消耗性能

appendfsync everysec #每秒执行一次,可能会丢失1s文件

appendfsync no #不执行sync,这个时候操作系统自己同步数据,速度最快!


## Redis持久化

### RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 Snapshot快照,它恢复时是将快照文件直接读到内存里。Reds会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临件替换上次持久化好的文件。

整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128141400.png" style="zoom: 67%;" />

>如何恢复rdb文件

1、只需要将rdb文件放在我们 Redis启动目录就可以, redish启动的时候会自动检查dump.rdb恢复其中的数据@

config get dir
"dir"
"usr/local/bin" #rdb文件


>几乎就他自己默认的配置就够用了,但是我们还是需要去学习!

#### **优点**:

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

#### **缺点**:

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

### AOF

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

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128141702.png" style="zoom: 80%;" />

以日志的形式来记录每个写操作将 Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件, redis启动之初会读取该文件重新构建数据,换言之, redise重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

AOf保存的是 appendonly aof文件

![](https://gitee.com/gao666666/images/raw/master/images/20211128141829.png)



AOF默认不开启,我们只需要将 appendorly改为yes就开启了aof!

如果这个aof文件有错位,这时候 redis是启动不起来的吗,我们需要修复这个aof文件

redis给我们提供了一个工具 redis- check-aof

#### **优点:**

appendfsync always #每次修改都会sync,消耗性能

appendfsync everysec #每秒执行一次,可能会丢失1s文件

appendfsync no #不执行sync,这个时候操作系统自己同步数据,速度最快!




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

#### **缺点:**

1. 相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢
2. Aof运行效率也要比db慢,所以我们 redis=默认的配置就是rdb持久化!

#### **RDB&AOF对比**

1. RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储

2. AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以 Redis议追加保存每次写的操作到文件末尾, Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大

3. 只做缓存,如果你只希望你的数据在服务器运行的时侯存在,你也可以不使用任何持久化

4. 同时开启两种持久化方式

   - 在这种情况下,当 redise重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情況下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
   - RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。

5. 性能建议

   - 因为RDB文件只用作后备用途,建议只在Save上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save901这条规则。
   - 如如果 Enable AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的lO,二是 AOF rewrite的最后将 rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值
   - 如果不 Enable AOF,仅靠 Master- Slave Repllcation实现高可用性也可以,能省掉一大笔IO,也减少了 rewrite时带来的系统波动。代价是如果 Master/ Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/ Slave中的RDB文件,载入较新的那个,微博就是这种架构。

   

## Redis发布订阅

Redis发布订(pub/suり)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis客户端可以订阅任意数量的频道
订阅/发布消息图

第一个:消息发送者,第二个:频道第三个:消息订阅者!

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128145025.png" style="zoom:80%;" />



下图展示了频道 channel1,以及订阅这个频道的三个客户端一- client2、 client.5和 client1之间的关系

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128145306.png" style="zoom:80%;" />

当有新消息通过 PUBLISH命令发送给频道 channel1时,这个消息就会被发送给订阅它的三个客户端

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128145327.png" style="zoom:80%;" />



> 命令

![](https://gitee.com/gao666666/images/raw/master/images/20211128145415.png)





生产者

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128145938.png" style="zoom:50%;" />



消费者

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128145900.png" style="zoom:50%;" />

> 原理

Reds是使用C实现的,通过分析 Redis i源码里的 pubsub. c文件,了解发布和订阅机制的底层实现,籍此加深对 Redis I的理解。

Redis通过 PUBLISH、 SUBSCRIBE和 PSUBSCRIBE等命令实现发布和订阅功能。

通过 SUBSCRIBE命令订阅某频道后, redis- server里维护了ー个字典,字典的键就是一个个 channel,而字典的值则是一个链表,链表中保存了所有订阅这个 channel的客户端。 SUBSCRIBE命令的关键,就是将客户端添加到给定 channel的订阅链表中。

通过 PUBLSH命令向订阅者发送消息, redis- server会使用给定的频道作为键,在它所维护的 channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者

Pub/Sub从字面上理解就是发布( Publish)与订阅( Subscribe),在 Redist中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。



> 使用场景

1. 实时消息系统!
2. 实时聊天!(频道当做聊天室,将信息回显给所有人即可!)
3. 订阅,关注系统都是可以的!

## Redis主从复制

主从复制,是指将一台 Redisi服务器的数据,复制到其他的 Redish服务器。前者称为主节点( master/ leader),后者称为从节点( slave/ follower);数据的复制是单向的,只能由主节点到从节点。 Master以写为主, Slave以读为主。

默认情况下,每台 Redis服务器都是主节点;且ー个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。主从复制的作用主要包括

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



<img src="https://gitee.com/gao666666/images/raw/master/images/20211128151610.png" style="zoom:80%;" />

主从复制,读写分离!80%的情況下都是在进行读操作!減缓服务器的压カ!架构中经常使用!一主二从

> 配置主从复制

127.0.0.1:6379> info replication #查看当前库的主从复制信息

Replication

role:master #角色
connected_slaves:0 #从机数量
master_replid:c5a2401371161abde39b924974c927d1f4343aea
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个配置文件,然后修改对应的信息
1、端口
2、pid名字
3、Iog文件名字
4、dump.rdb名字

修改完毕之后,启动我们的3个edis服务器,可以通过进程信息查看

![](https://gitee.com/gao666666/images/raw/master/images/20211128152921.png)



### **一主二从**

<strong style="color:red;">默认情况下,每台 Redise服务器都是主结点</strong>

我们一般情况下只用配置从机就好了!

进入从机
SLAVEOF 127.0.0.1:6379 #SLAVEOF host port 找谁当主机
127.0.0.1: 6380> info replication
Replicationrole: slave
master host: 127.0.0.1master_port: 6379master_link-status: up
master__1o_seconds__ago: 3master_sync_in-progress: 0slave repl offset: 14slave_priority: 100slave_read_only: 1connected slaves: 0
master_replid: a81be8dd257636b2d3e7a9f595e69d73ff03774e
master_ replid2:0000000000000000000000000000000000000000master repl offset: 14
second_repl_offset: -1repl_backlog_active: 1rep1_backlog_size: 1048576
repl_backlog_first_byte_offset: 1rep_backlog_histlen: 14


主机中

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128153817.png" style="zoom:80%;" />





### **细节**

主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机保存!

主机写:

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128154614.png" style="zoom:80%;" />

从机读:

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128154652.png" style="zoom: 80%;" />

测试:主机断开连接,从机依旧连接到主机的,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息!

如果是使用命令行,来配置的主从,这个时候如果重启了,就会变回主机!只要变为从机,立马就会从主机中获取值!

> 复制原理

Slave启动成功连接到 master后会发送一个sync命令

Master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,<strong style="color:red;"> master将传送整个数据文件到 slave,并完成一次完全同步。</strong>

全量复制:而 slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

增量复制: Master继续将新的所有收集到的修改命令依次传给 slave,完成同步

但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行



> 链路

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128155614.png" style="zoom:80%;" />

> 如果没有主机这时候能不能选出一个老大

SLAVEOF NO ONE


如果主机断开了连接,我们可以使用 SLAVEOF no one让自己变成主机!其他的节点就可以手动连接到最新的这个主节点(手动)



### **哨兵模式**

> 概述

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切換为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。 Redis/从2.8开始正式提供了 Sentinel(哨兵)架构来解决这个问题

能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

哨兵模式是一种特殊的模式,首先 Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待 Redis服务器响应,从而监控运行的多个 Redis:实例。

 



哨兵模式还有两个作用

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

然而一个哨兵进程对 Redisr服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128160516.png" style="zoom:67%;" />

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover【故障转移】操作。切換成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

> 测试

当前状态是一主二从

1. 首先创建哨兵配置文件sentinel.conf

#sentinel monitor 主机名 host port 1
sentinel monitor myredis 127.0.0.1 6379 1


后面的这个数字1,代表主机挂了, slave投票看让谁接替成为主机,票数最多的,就会成为主机

2. 启动哨兵

redis-sentinel kconfig/sentinel. conf


3. 如果Master节点断掉,就会从从节点中随机选择一个服务器(这里面有一个投票算法)

哨兵日志

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128161414.png" style="zoom:67%;" />

如果主机此时回来了,只能归并到新的主机午,当做从机,这就是哨兵模式的规则!

> 哨兵模式

优点

1. 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有

2. 主从可以切换,故障可以转移,系统的可用性就会更好

3. 哨兵模式就是主从模式的升级,手动到自动,更加健壮

缺点

1.  Redis不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
2.  实现哨兵模式的配置其实是很麻烦的,里面有很多选择

## Redis缓存穿透和雪崩(面试高频,工作常用)

### 缓存穿透

缓存穿透的概念很简单,用户想要查询一个数据,发现 ,redis!内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次査询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成艮大的压力,这时候就相当于出现了缓存穿透。

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128162551.png" style="zoom:67%;" />

> 解决方案

#### 布隆过滤器

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

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128162756.png" style="zoom:67%;" />

#### 缓存空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128162843.png" style="zoom:67%;" />

但是这种方法会存在两个问题

1. 如果空值能够被缓存起来,这就意味着存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

### 缓存击穿

> 概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访可,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访词,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来査询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

> 解决方案

##### 设置热点数据永不过期

从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的可题。

##### 加互斥锁

分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

<img src="https://gitee.com/gao666666/images/raw/master/images/20211128163302.png" style="zoom:67%;" />

### 缓存雪崩

> 概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。

产生雪崩的原因之ー,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了绶存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

![](https://gitee.com/gao666666/images/raw/master/images/20211128163436.png)    

> 解决方案

##### redist高可用

这个思想的含义是,既然 redis有可能挂掉,那我多增设几台 'redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)

##### 限流降级(在 Spring Cloudi讲解过!)

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

##### 数据预热
标签: none

非特殊说明,本博所有文章均为博主原创。

评论啦~



唉呀 ~ 仅有一条评论


  1. 笔记 - GA666666 Blog ~ 个人博客
    笔记 - GA666666 Blog ~ 个人博客

    [...]Docker容器 -2021Dubbo框架 -2020Git版本控制-20180Golang语言入门-2021Java设计模式-2019JVM探究-2019Mybatis框架-2019MySQL数据库-2019Netty网络编程框架-2021Redis数据库-2020SMS框架整合-2019Spring框架-2018Springboot框架-2019Springmvc框架-2019SVN版本控制-[...]

    回复 2024-02-21 11:10