redis命令,SpringBoot整合Redis6,主从复制,哨兵模式,集群,springCache初高级应用。
1. Docker安装Redis,以及初从配置参考:reids安装2. Redis的基础2.1 redis的key命令keys *查看当前数据库所有keyexists key 判断某个key是否存在type key查看key的数据类型del key删除指定的key类型unlink key 异步删除key(仅将key从keyspace元数据删除,真正的删除后再后续操作中进行)expire key &
目录
1. Docker安装Redis
docker启动redis默认是没有redis.conf的配置文件的.想要使用redis.conf方式启动采用下面步骤
- 1. 从官网下载redis.conf文件
- 2. 启动将你的redis.conf放到/root/config/文件下
- 3.启动redis
- 4.如果你不用配置文件方式启动,到最后redis配置不了哨兵模式
docker run --name redis -p 6380:6379 \ -v /root/config/redis.conf:/etc/redis/redis.conf \ -v /root/config:/data \ -d redis:6.0.15 redis-server /etc/redis/redis.conf
2. Redis的基础
2.1 redis的key命令
- keys * 查看当前数据库所有key
- exists key 判断某个key是否存在
- type key 查看key的数据类型
- del key 删除指定的key类型
- unlink key 异步删除key(仅将key从keyspace元数据删除,真正的删除后再后续操作中进行)
- expire key <Time> 给制定的key设置过期时间(单位:秒)
- ttl key 查看还有多少时间过期(-1:永不过期。-2:已过期。其他表示剩余秒数)
- select <database> 切换数据库
- dbsize 查看当前数据库的key数量
- flushdb 清空当前数据库
- flushall 通杀全部数据库
2.2 reids的数据结构(6.0新增的数据结构)
注意每个命令之间的空格 , 注意命令之间没有符号,都是空格
1. String(字符串)类型
介绍:最基本的key Value 类型,String类型是二进制安全的可以存数任何数据,包括图片,视频等。Value最多可存储512M大小。
命令:
set <key> <value> 保存 get <key> 获取 append <key> <value> 给指定的key对应的Value追加数据到末尾 strlen <key> 获取值的长度 incr <key> 将key中存储的数字值加一(如果为空则新增值为1) decr <key> 将key中存储的数字减一 incrby / decrbu 自定义加减步长 mset <key> <value> <key> <value>... 保存多个key-Value mget <key> <key> ... 取出多个key的值 msetnx 等同于mset的作用,但是该命令是原子操作(保存的key必须都不存在) setex <key> <过期时间> <value> 在保存时候同时设置过期时间(单位:秒) getset <key> <set> 更新值 getrange <key> <索引起始> <索引结束> 截取值,获取截取后的值 setrange <key> <索引起始位置> <value> 覆盖指定位置的值
2. List(列表)类型
介绍:
- list类型是字符串列表,底层是一个双向列表,对两端的操作性能较高,你可以添加或者删除一个元素到头部或者尾部。
- list在元素较少的情况下,会使用一块连续的内存存储,这个结构是ziplist,就是压缩列表,当数据较多时候才会快速转成列表。即快速链表quicklist.
命令:
lpush / rpush <key> <value1> <value2>... 从左/从右插入一个或者多个数据 lpop / rpop <key> 从左 / 从右取出一个值 rpoplpush <key1> <key2> 从key1列表右边取出一个值,插入到key2左边 lrange <key> <start> <end> 根据索引下标获取数据(从左到右,0 -1 表示获取所有) lindex <key> <index> 按照索引获取对应的元素 llen <key> 获取列表的长度 linsert <key> before <value> <newvalue> 在Value后面插入newvalue lrem <key> <n> <value> 从左删除n个value lset <key> <index> <value> 将列表key下表index的值替换为value
3. set(集合)类型
介绍:
- set类型保存数据后,自动排序(数字value)去重。
- set的数据结构是dict字典,而字典使用哈希表实现的。和java中的HashSet的结构一样,内部使用hash结构,所有的value都指向同一个内部对象。
命令:
sadd <key> <value1> <value2> ... 添加一个或者多个member元素,重复元素会被忽略。 smembers <key> 取出该集合的所有值 sismember <key> <value> 判断key中是否幼value值,有返回1,无返回0 scard <key> 返回该集合的元素个数 srem <key> <value1> <value2>... 删除集合中的元素 spop <key> 随机从该集合中获取个值 srandmember <key> <n> 随机从集合中获取n个值。
4. Hash(哈希)累型:
介绍:
- hash 是一个键值对集合。相似与java中的Map集合。特别适用于存储对象,
- Hash类型对应的数据结构是两种: ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
命令:
hset <key> <field> <value> (保存) 给集合中的 <field> 复制<value> hget <key> <field> (获取)从<key>集合中获取<filed>的值 hmset <key> <field> <value> <filed> <value> ... (批量保存)一个key中可以保存多个field,value hexists <key> <field> 查看可以中是否存在指定的field hkeys <key> 查询key中的所有filed hvals <key> 列出该hash集合的所有value hincrby <key> <field> <incr> 对Value加 incr (incr表示数字,如果incr等于一,那么就给value加上1) hsetnx <key> <field> <value> 等同于hincrby命令(如果咩有该field就不加)
5. Zset (有序集合)类型
介绍:
- zset 与普通集合set非常相似,是一个没有重复元素的字符串集合。v不同之处是有序集合的每个成员都关联了一个评分( score ) ,这个评分( score )被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了。
- 因为元素是有序的,所以你也可以很快的根据评分( score)或者次序( position )来获取一个范围的元素。
- 访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
命令:
zadd <key> <score1> <value1> <score2> <value2> ...添加一个或者多个元素到有序集合中。(score表示评分,必须数字类型。zset会根据评分排序降序排序) zrange <key> <start> <end> [WITHSCORES] 返回有序集key中,下标(索引)在<start><end>之间的元素,带WITHSCORES可以将分数和值一起返回到结果集中。 zincrby <key> <increment> <value> 为指定元素的score分数加上increment zrem <key> <value> 删除该集合下,指定值的元素 zcount <key> <ScoreMin> <ScoreMax> 统计该集合,分数区间内的元素个数 zrank <key> <value> 返回该值在集合中的排名,从0开始。
redis6的新数据类型
bitmaps (位操作类型) :
介绍:
- Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
- Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
命令:
setbit <key> <offset> <value> 设置Bitmaps中某个偏移量的值(0或1) getbit <key> <offset> 获取Bitmaps中某个偏移量的值 bitcount <key> <start> <end> 查询value为1的个数,(不指定start,end表示查询所有,指定表示只对指定的start和end范围内查询,start和end表示 字节组下标,-1表示最后一位,-2表示倒数第二位), bitop <and / or /not / xor> <key1> <key2> bitop是一个复合操作,and表示查询交集,or并集,not 非,xor异或
HyperLogLog(基数操作类型):
介绍:
- 该类型是做基数统计的算法,HyperLogLog的优点是,在输入的元素数量非常大时,计算基数所需要的空间总是固定的,
- 每个HyperLogLog键只需要花费12kb内存,既可以计算2^64个不同元素的基数。
命令:
pfadd <key> <value...> 可以保存多个value到HyperLogLog中,注意每个value之间的空格(重复的元素添加不进去) pfcount <key> 计算基数(元素的格式) pfmerge <key> <key1> <key2> 合并元素,将key1,和key2中的元素取出来合并到key中
Geospatial (地理位置操作的类型)
介绍:
Geospatial类型提供了对经纬度地理位置操作的方法,包括范围查询,距离查询,经纬度hash操作等(都是2维平面查询,不是球面查询,不是球面查询)。
命令:
#注意有效的经度在-180 到 180 维度是 -85.05112878 到 85.05112878,超过该范围会报错 geoadd <key> <经度> <维度> <名称> 添加地理位置信息 (一次可添加多个) geopos <key> <名称> 根据名称取出经纬度信息 geodist <key> <名称1> <名称2> <m / km /mi / ft> 获取俩个位置的距离 m:返回米,km:返回千米,mi:返回英里 ,ft:返回英尺 <!--根据经纬度为坐标,半径为范围构成一个圆形,返回key中被该范围包含的信息。 (注意:该范围查询是二位平面查询,跟球面范围查询有差异) 最后一个命令是指定半径的单位 --> georadius <key> <经度> <维度> <半径距离> <m / km /mi / ft>
3.reids的配置文件
docker启动的redis默认没有redis.conf配置文件
#注释掉bind 127.0.0.1,使redis可以外部访问 bind 0.0.0.0 # 端口号 port 6381 #给redis设置密码 #requirepass red ##redis持久化 默认是no appendonly yes #开启protected-mode保护模式,需配置bind ip或者设置访问密码 #关闭protected-mode模式,此时外部网络可以直接访问 protected-mode no #用守护线程的方式启动 daemonize no #防止出现远程主机强迫关闭了一个现有的连接的错误 默认是300 tcp-keepalive 300 #设置客户端连接时的超时时间,单位为秒。当客户端在这段时间内没有发出任何指令,那么关闭该连接 timeout 0 tcp-backlog 511 supervised no pidfile "/var/run/redis_6379.pid" #日志级别 (debug,verbose, notice, 和warning。生产环境下一般开启notice) loglevel notice #redis生成的日志(docker容器内的目录,不是宿主机的目录) logfile "/data/redis-s2.log" #默认数据库数量 databases 16 always-show-logo yes # RDB持久化策略(默认开启) #900秒后 有一个key变化就持久话 #300秒后 有10个key变化就持久话 #60秒后 有10000个key变化就持久话 save 900 1 save 300 10 save 60 10000 #yes : RDB快照保存失败后 客户端不可想redis写入,只可读 #no : 禁用此功能 stop-writes-on-bgsave-error yes # 保存 .rdb持久化文件时 是否使用LZF压缩 yes开启 no关闭 rdbcompression yes #是否检查rdb快照的完整性,损失大概 百分之十 的性能 rdbchecksum yes #rdb持久化生成的默认文件名 dbfilename "dump.rdb" #在未启用持久性的情况下删除复制使用的 RDB 文件。 #默认情况下,此选项被禁用,但是在某些环境中,出于法规或其他安全考虑, #RDB 文件应由 master 持久保存在磁盘上以提供副本,或由副本存储在磁盘上以加载它们以进行初始同步。 #尽快删除。请注意,此选项仅适用于同时禁用 AOF 和 RDB 持久性的实例,否则将被完全忽略。 #获得相同效果的另一种(有时是更好的)方法是在主实例和副本实例上使用无盘复制。然而,在副本的情况下,无盘并不总是一种选择。 rdb-del-sync-files no #redis的工作目录(持久化文件和日志生成后保存的目录) dir "/data" #当副本失去与主服务器的连接时,或者当复制仍在进行中时,副本可以以两种不同的方式进行操作: #1)如果副本服务陈旧数据设置为“是”(默认值)副本仍然会回复客户端请求,可能带有过期数据,或者如果这是第一次同步,数据集可能只是空的。 #2) 如果replica-serve-stale-data 设置为'no',则replica 将对所有类型的命令回复错误“SYNC with master in progress”, #但对INFO、replicaOF、AUTH、PING、SHUTDOWN、REPLCONF、角色、配置、订阅、取消订阅、订阅、取消订阅、发布、发布订阅、命令、发布、主机:和延迟。 replica-serve-stale-data yes #您可以配置副本实例以接受或不接受写入。 #就是主从复制中,slave节点是否可以写入数据(yes:不能写入;no:可以写入) replica-read-only yes #当使用无盘复制时,master 在开始传输之前等待一段可配置的时间(以秒为单位), #希望多个副本到达并且传输可以并行化。对于慢速磁盘和快速(大带宽)网络,无盘复制效果更好。 repl-diskless-sync no #启用无盘复制后,可以配置服务器等待的延迟,以便生成通过套接字将 RDB 传输到副本的子节点。 #因为一旦传输开始,就不可能为到达的新副本提供服务,新副本将排队等待下一次 RDB 传输,因此服务器等待延迟以让更多副本到达。 #延迟以秒为单位指定,默认为 5 秒。要完全禁用它,只需将其设置为 0 秒, repl-diskless-sync-delay 5 # In many cases the disk is slower than the network, and storing and loading # the RDB file may increase replication time (and even increase the master's # Copy on Write memory and salve buffers). # However, parsing the RDB file directly from the socket may mean that we have # to flush the contents of the current database before the full rdb was # received. For this reason we have the following options: # # "disabled" - Don't use diskless load (store the rdb file to the disk first) # "on-empty-db" - Use diskless load only when it is completely safe. # "swapdb" - Keep a copy of the current db contents in RAM while parsing # the data directly from the socket. note that this requires # sufficient memory, if you don't have it, you risk an OOM kill. repl-diskless-load disabled # Disable TCP_NODELAY on the replica socket after SYNC? # # If you select "yes" Redis will use a smaller number of TCP packets and # less bandwidth to send data to replicas. But this can add a delay for # the data to appear on the replica side, up to 40 milliseconds with # Linux kernels using a default configuration. # # If you select "no" the delay for data to appear on the replica side will # be reduced but more bandwidth will be used for replication. # # By default we optimize for low latency, but in very high traffic conditions # or when the master and replicas are many hops away, turning this to "yes" may # be a good idea. repl-disable-tcp-nodelay no # The replica priority is an integer number published by Redis in the INFO # output. It is used by Redis Sentinel in order to select a replica to promote # into a master if the master is no longer working correctly. # # A replica with a low priority number is considered better for promotion, so # for instance if there are three replicas with priority 10, 100, 25 Sentinel # will pick the one with priority 10, that is the lowest. # # However a special priority of 0 marks the replica as not able to perform the # role of master, so a replica with priority of 0 will never be selected by # Redis Sentinel for promotion. # # By default the priority is 100. replica-priority 100 # ACL LOG # # The ACL Log tracks failed commands and authentication events associated # with ACLs. The ACL Log is useful to troubleshoot failed commands blocked # by ACLs. The ACL Log is stored in memory. You can reclaim memory with # ACL LOG RESET. Define the maximum entry length of the ACL Log below. acllog-max-len 128 # Using an external ACL file # # Instead of configuring users here in this file, it is possible to use # a stand-alone file just listing users. The two methods cannot be mixed: # if you configure users here and at the same time you activate the exteranl # ACL file, the server will refuse to start. # # The format of the external ACL user file is exactly the same as the # format that is used inside redis.conf to describe users. # # aclfile /etc/redis/users.acl # Command renaming (DEPRECATED). # # ------------------------------------------------------------------------ # WARNING: avoid using this option if possible. Instead use ACLs to remove # commands from the default user, and put them only in some admin user you # create for administrative purposes. # ------------------------------------------------------------------------ # # It is possible to change the name of dangerous commands in a shared # environment. For instance the CONFIG command may be renamed into something # hard to guess so that it will still be available for internal-use tools # but not available for general clients. # # Example: # # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 # # It is also possible to completely kill a command by renaming it into # an empty string: # # rename-command CONFIG "" # # Please note that changing the name of commands that are logged into the # AOF file or transmitted to replicas may cause problems. ################################### 客户端 #################################### #客户端的最大链接 # maxclients 10000 ############################## 内存管理 ################################ #redis可以占用的内存 单位字节 #当达到内存限制时,Redis 将尝试根据选择的驱逐策略删除键 #maxmemory <bytes> # redis过期key的移除策略 #volatile-lru:从已设置过期时间的key集中,挑选最近最少使用的数据淘汰。 #volatile-ttl:从已设置过期时间的key集中,挑选将要过期的数据淘汰。 #volatile-random:从已设置过期时间的key集中,随机选择数据淘汰。 #volatile-lfu:从已设置过期时间的key集中,挑选使用频率最低的数据淘汰。 #allkeys-lru:从所有key集中,挑选最近最少使用的数据淘汰 #allkeys-lfu:从所有key集中,挑选使用频率最低的数据淘汰。 #allkeys-random:从所有key集中,(server.db[i].dict)随机选择数据淘汰 #noeviction:不进行移除。针对写操作,只是返回错误信息 maxmemory-policy noeviction #LRU、LFU 和最小 TTL 算法不是精确算法,而是近似算法(为了节省内存),因此您可以对其进行调整以提高速度或准确性。 #对于默认 Redis 将检查五个键并选择最近使用较少的一个,您可以使用以下配置指令更改样本大小。 #默认值 5 会产生足够好的结果。 10 非常接近真实的 LRU,但 CPU 成本更高。 3更快但不是很准确。 # maxmemory-samples 5 #从 Redis 5 开始,默认情况下,redis主从复制环境中,salve节点会忽略 maxmemory 设置 # 除非在发生 failover 后,slave此节点被提升为 master 节点。 #这意味着只有 master 才会执行过期删除策略。 #并且 master 在 删除键之后会对 所有slave 发送 DEL (删除)命令。 # replica-ignore-maxmemory yes #设置过期keys仍然驻留在内存中的比重,默认是为1,表示最多只能有10%的过期key驻留在内存中, #该值设置的越小,那么在一个淘汰周期内,消耗的CPU资源也更多,因为需要实时删除更多的过期key。 #所以该值的配置是需要综合权衡的。 # active-expire-effort 1 ############################# LAZY FREEING #################################### #针对redis内存使用达到 maxmemory,并设置有淘汰策略时,在被动淘汰键时,是否采用lazy free机制。 #因为此场景开启lazy free, 可能使用淘汰键的内存释放不及时,导致redis内存超用,超过maxmemory的限制。 lazyfree-lazy-eviction no #针对设置有TTL的键(过期时间),达到过期后,被redis清理删除时是否采用lazy free机制。此场景建议开启,因TTL本身是自适应调整的速度。 lazyfree-lazy-expire no #针对有些指令在处理已存在的键时,会带有一个隐式的DEL键的操作。 #如rename命令,当目标键已存在,redis会先删除目标键, #如果这些目标键是一个big key,那就会引入阻塞删除的性能问题。 #此参数设置就是解决这类问题,建议可开启。 lazyfree-lazy-server-del no #slave进行全量数据同步,slave在加载master的RDB文件前,会运行flushall来清理自己的所有数据, #该配置决定是否采用异常flush机制。如果内存变动不大,建议可开启。 #可减少全量同步耗时,从而减少主库因输出缓冲区爆涨引起的内存使用增长。 replica-lazy-flush no #当用 UNLINK 调用替换用户代码 DEL 调用并不容易时,也可以使用以下配置指令将 DEL 命令的默认行为修改为与 UNLINK 完全相同: lazyfree-lazy-user-del no # AOF持久化生成的文件名 appendfilename "appendonly.aof" #aof文件刷新的频率。有三种: #1.no 依靠OS进行刷新,redis不主动刷新AOF,这样最快,但安全性就差。 #2.always 每提交一个修改命令都调用fsync刷新到AOF文件,非常非常慢,但也非常安全。 #3.everysec 每秒钟都调用fsync刷新到AOF文件,很快,但可能会丢失一秒以内的数据。 appendfsync everysec #指定是否在后台aof文件rewrite期间调用fsync, #默认为no,表示要调用fsync(无论后台是否有子进程在刷盘)。 #Redis在后台写RDB文件或重写AOF文件期间会存在大量磁盘IO,此时,在某些linux系统中,调用fsync可能会阻塞。 no-appendfsync-on-rewrite no #aof文件增长比例,指当前aof文件比上次重写的增长比例大小。aof重写即在aof文件在一定大小之后, #重新将整个内存写到aof文件当中,以反映最新的状态(相当于bgsave)。这样就避免了,aof文件过大 #而实际内存数据小的问题(频繁修改数据问题)。 auto-aof-rewrite-percentage 100 #aof文件重写最小的文件大小,即最开始aof文件必须要达到这个文件时才触发, #后面的每次重写就不会根据这个变量了(根据上一次重写完成之后的大小).此变量仅初始化启动redis有效. #如果是redis恢复时,则lastSize等于初始aof文件大小。 auto-aof-rewrite-min-size 64mb # 指redis在恢复时,会忽略最后一条可能存在问题的指令。 #默认值yes。即在aof写入时,可能存在指令写错的问题,这种情况下,yes会log并继续,而no会直接恢复失败。 aof-load-truncated yes #在开启了这个功能之后,AOF重写产生的文件将同时包含RDB格式的内容和AOF格式的内容, #其中RDB格式的内容用于记录已有的数据,而AOF格式的内存则用于记录最近发生了变化的数据, #这样Redis就可以同时兼有RDB持久化和AOF持久化的优点 #(既能够快速地生成重写文件,也能够在出现问题时,快速地载入数据)。 aof-use-rdb-preamble yes ################################ LUA SCRIPTING ############################### #一个Lua脚本最长的执行时间,单位为毫秒,如果为0或负数表示无限执行时间,默认为5000 lua-time-limit 5000 ################################ REDIS 集群 ############################### #如果是yes,表示启用集群,否则以单例模式启动 # cluster-enabled yes #这不是一个用户可编辑的配置文件,这个文件是Redis集群节点自动持久化每次配置的改变,为了在启动的时候重新读取它 # cluster-config-file nodes-6379.conf #超时时间,集群节点不可用的最大时间。如果一个master节点不可到达超过了指定时间,则认为它失败了。 #注意,每一个在指定时间内不能到达大多数master节点的节点将停止接受查询请求 # cluster-node-timeout 15000 #如果设置为0,则一个slave将总是尝试升级为master。 #如果设置为一个正数,那么最大失去连接的时间是node timeout乘以这个factor。 # cluster-replica-validity-factor 10 #一个master和slave保持连接的最小数量(即:最少与多少个slave保持连接), #也就是说至少与其它多少slave保持连接的slave才有资格成为master # cluster-migration-barrier 1 #如果设置为yes,这也是默认值,如果key space没有达到百分之多少时停止接受写请求。 #如果设置为no,将仍然接受查询请求,即使它只是请求部分key # cluster-require-full-coverage yes # 此选项设置为yes时,可防止从设备尝试对其进行故障转移master在主故障期间。 然而,仍然可以强制执行手动故障转移 # cluster-replica-no-failover no # 是否允许集群在宕机时读取 # cluster-allow-reads-when-down no ########################## docker集群/NAT支持 ######################## # 集群节点ip, # 我们使用docker搭建redis集群,这里ip一定要写成宿主机的IP不然Java启动连接不到redis # cluster-announce-ip 10.1.1.5 # 集群节点端口 # cluster-announce-port 6379 # 集群总线端口, # docker搭建redis集群这里也要开放,集群总线端口默认就是redis+ 1000 # 举例:上面我们规定了端口为6379,而这个集群总线端口应该写成16379 # cluster-announce-bus-port 6380 ################################## SLOW LOG ################################### #决定要对执行时间大于多少微秒(microsecond,1秒 = 1,000,000 微秒)的查询进行记录 slowlog-log-slower-than 10000 # 它决定 slow log 最多能保存多少条日志, slow log 本身是一个 FIFO 队列,当队列大 #小超过 slowlog-max-len 时,最旧的一条日志将被删除,而最新的一条日志加入到 slow log ,以此类推。 slowlog-max-len 128 # 能够采样不同的执行路径来知道redis阻塞在哪里。这使得调试各种延时问题变得简单, #设置一个毫秒单位的延时阈值来开启延时监控。 latency-monitor-threshold 0 # By default all notifications are disabled because most users don't need # this feature and the feature has some overhead. Note that if you don't # specify at least one of K or E, no events will be delivered. notify-keyspace-events "" ############################### ADVANCED CONFIG ############################### # 哈希在条目数量较少且最大条目不超过给定阈值时使用内存高效数据结构进行编码。可以使用以下指令配置这些阈值。 #ziplist最大条目数 hash-max-ziplist-entries 512 #ziplist单个条目value的最大字节数 hash-max-ziplist-value 64 # ziplist列表最大值,默认存在五项: # -5:最大大小:64 Kb <——不建议用于正常工作负载 # -4:最大大小:32 Kb <——不推荐 # -3:最大大小:16 Kb <——可能不推荐 # -2:最大大小:8 Kb<——很好 # -1:最大大小:4 Kb <——好 # 正数意味着存储最多 _exactly_ 该数量的元素 # 正数意味着每个列表节点最多存储_exactly_该数量的元素。性能最高的选项通常是 -2(8 Kb 大小)或 -1(4 Kb 大小) list-max-ziplist-size -2 # 一个quicklist两端不被压缩的节点个数。0: 表示都不压缩。这是Redis的默认值, # 1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。3: 表示quicklist两 # 端各有3个节点不压缩,中间的节点压缩。 list-compress-depth 0 #set集合仅在一种情况下具有特殊编码:当集合仅由字符串组成时,这些字符串恰好是 64 位有符号整数范围内的基数为 10 的整数。 #以下配置设置设置了集合大小的限制,以便使用这种特殊的内存节省编码。 set-max-intset-entries 512 # 与哈希和列表类似,排序集也经过特殊编码以节省大量空间。仅当排序集的长度和元素低于以下限制时,才使用此编码: zset-max-ziplist-entries 128 zset-max-ziplist-value 64 #value大小 #小于等于hll-sparse-max-bytes使用稀疏数据结构(sparse), #大于hll-sparse-max-bytes使用稠密的数据结构(dense) hll-sparse-max-bytes 3000 # Streams单个节点的字节数,以及切换到新节点之前可能包含的最大项目数。 stream-node-max-bytes 4kb stream-node-max-entries 100 # 主动重新散列每100毫秒CPU时间使用1毫秒,以帮助重新散列主Redis散列表(将顶级键映射到值 activerehashing yes # 对客户端输出缓冲进行限制可以强迫那些不从服务器读取数据的客户端断开连接,用来强制关闭传输缓慢的客户端。 client-output-buffer-limit normal 0 0 0 #对于slave client和MONITER client,如果client-output-buffer一旦超过256mb,又或者超过64mb持续60秒,那么服务器就会立即断开客户端连接 client-output-buffer-limit replica 256mb 64mb 60 #对于pubsub client,如果client-output-buffer一旦超过32mb,又或者超过8mb持续60秒,那么服务器就会立即断开客户端连接 client-output-buffer-limit pubsub 32mb 8mb 60 # 客户端查询缓冲区累积新命令。 默认情况下,它被限制为固定数量,以避免协议失步(例如由于客 # 户端中的错误)将导致查询缓冲区中的未绑定内存使用。 但是,如果您有非常特殊的需求,可以在 # 此配置它,例如我们巨大执行请求 # client-query-buffer-limit 1gb # 在Redis协议中,批量请求(即表示单个字符串的元素)通常限制为512 MB。 但是,您可以在此更改此限制 # proto-max-bulk-len 512mb # 默认情况下,hz设置为10.提高值时,在Redis处于空闲状态下,将使用更多CPU。范围介于1到500之间, # 大多数用户应使用默认值10,除非仅在需要非常低延迟的环境中将此值提高到100 hz 10 # 启用动态HZ时,实际配置的HZ将用作基线,但是一旦连接了更多客户端,将根据实际需要使用配置的HZ值的倍数 dynamic-hz yes # 当一个子进程重写AOF文件时,如果启用下面的选项,则文件每生成32M数据会被同步 aof-rewrite-incremental-fsync yes # 当redis保存RDB文件时,如果启用了以下选项,则每生成32 MB数据将对文件进行fsync。 这对于以递增 # 方式将文件提交到磁盘并避免大延迟峰值非常有用 rdb-save-incremental-fsync yes # Jemalloc background thread for purging will be enabled by default jemalloc-bg-thread yes # Generated by CONFIG REWRITE user default on nopass ~* +@all
redis.conf配置文件详解
- 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
- 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
- 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。
- volatile-lru:从已设置过期时间的key集中,挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的key集中,挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的key集中,随机选择数据淘汰。
- volatile-lfu:从已设置过期时间的key集中,挑选使用频率最低的数据淘汰。
- allkeys-lru:从所有key集中,挑选最近最少使用的数据淘汰
- allkeys-lfu:从所有key集中,挑选使用频率最低的数据淘汰。
- allkeys-random:从所有key集中,(server.db[i].dict)随机选择数据淘汰
- noeviction:不进行移除。针对写操作,只是返回错误信息
- 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。
- 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。
4. Redis的持久化
redis持久化的配置
RDB配置:
#redis RDB持久化的文件名 dbfilename dump.rdb #redis工作目录,redis启动后生成的日志和持久化的文件都会在该目录生成 dir /data #RDB 持久化的配置,默认redis会开启RDB持久化 #900 秒(15 分钟)后 如果至少 1 个key更改 #300 秒(5 分钟)后 如果至少 10 个key更改 #60 秒后 如果至少 10000 个key更改 #然后就触发了RDB持久化 save 900 1 save 300 10 save 60 10000
RDB 持久化 save 和 bgsave命令的区别
- save:save时只管保存,其它不管,执行 save 命令前,Redis不能处理其它命令,直到 RDB 过程完成为止。
- bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
RDB持久化方式:
- 跟据指定的时间策略将,内存中的数据以快照的方式写入磁盘(Snapshot快照),它恢复时是将快照文件直接读到内存里.RDB可能会在最后一次持久化造成数据丢失,假如说在进行最后一次持久化的过程中,还没到指定的同步时间服务宕机了,停电了,这就导致数据的丢失了.
- redis在持久化的时候会开启一个fork子线程持久化,会先将数据写入到一个临时的文件内,等所有数据都被写入到文件中后,会将新文件替换掉旧文件,而只执行一次IO操作.如果要恢复大规模的数据,并且对数据的完整性要求不高,那么受用RDB要比AOF的方式效率高.
AOF配置:
#yes:开启redis的持久化。默认关闭 appendonly no # AOF持久化的文件名 appendfilename "appendonly.aof" #aof文件刷新的频率。有三种: #1.no 依靠OS进行刷新,redis不主动刷新AOF,这样最快,但安全性就差。 #2.always 每提交一个修改命令都调用fsync刷新到AOF文件,非常非常慢,但也非常安全。 #3.everysec 每秒钟都调用fsync刷新到AOF文件,很快,但可能会丢失一秒以内的数据。 appendfsync everysec
AOF的持久化方式
Redis的写命令都会被追加到AOF缓冲区内.AOF缓冲区根据AOF策略将缓存区的数据同步到AOF文件中,如果AOF文件大小超过重写策略,或者手动重写时. 就会对AOF文件重写压缩AOF文件的内存.
化 AOF采用文件追加的方式持久数据,为了防止文件越来越大,从而增加了重写机制,AOF文件的大小超过所设定的阈值时,fork出一条新进程来将文件重写(也是先写临时文件最后再rename).
AOF重写
为了减小aof文件的体量,可以手动发送“bgrewriteaof”指令,通过子进程生成更小体积的aof,然后替换掉旧的、大体量的aof文件。
这两个配置项的意思是,在aof文件体量超过64mb,且比上次重写后的体量增加了100%时自动触发重写
AOF重写原理
- AOF重写并不是在主线程中,而是redis会fork一个bgrewriteaof子进程.
- fork子进程的过程是要在主线程中执行的,这时候主线程需要拷贝内存页表,这个页表记录了虚拟内存和物理内存的映射关系,如果内存很大,拷贝过程花费的时间就会很大,而这个拷贝过程中主线程是阻塞的。
- fork子进程完成后,主线程和bgrewriteaof子进程使用的是同一块儿内存空间,这时如果有新的写请求到来,并且写命令是已经存在的key,主线程会使用CopyOnWrite的方式,为这个key申请新的内存空间,进行写操作。如果这个key是一个bigkey,那也会耗时很多。
5.springboot整合redis
pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
yml配置
spring: redis: database: 0 timeout: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: root pool: max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制) max-idle: 8 # 连接池中的最大空闲连接 min-idle: 2 # 连接池中的最小空闲连接
客户端工具操作Redis(redisTemplate )
java代码实现
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.test.context.junit4.SpringRunner; import java.sql.ResultSet; @SpringBootTest @RunWith(SpringRunner.class) public class RedisTest { //保存到redis就是二进制 @Autowired private RedisTemplate redisTemplate; //StringRedisTemplate 采用String的序列化方式 @Autowired private StringRedisTemplate stringRedisTemplate; @Test public void stringTest(){ //参数一:key 参数二:value 参数三: 有效时间(单位秒,不设置表示永久有效) redisTemplate.opsForValue().set("key","value"); stringRedisTemplate.opsForValue().set("key1","value1"); } }
redis中保存的结果.
Redis自定义序列化方式
为了使存入redis的数据有跟好的直观性,我们重写redis的序列化方式.
/** * 自定义序列化机制 */ @Configuration public class SerializerConfig { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setDefaultSerializer(serializer); return template; }
其他类型案例
@Autowired private RedisTemplate redisTemplate; @Test public void stringSaveTemplate(){ //插入key 和 value , 有效时间,时间单位 redisTemplate.opsForValue().set("key1","redisTemplate工具插入的测试数据",5000, TimeUnit.MILLISECONDS); //绑定key,其后的操作都操作该key中的数据 BoundValueOperations key1 = redisTemplate.boundValueOps("key1"); key1.append("追加数据"); //该方法会覆盖之前的数据 key1.set(123); //获取数据, Object data = key1.get(); System.out.println(data.toString()); //value减一 Long decrement = key1.decrement(); } @Test public void hashSaveTemplate(){ //保存 HashMap<String, Object> hashMap = new HashMap<>(); hashMap.put("name","张三"); hashMap.put("age",12); redisTemplate.opsForHash().putAll("key2",hashMap); //绑定key BoundHashOperations key2 = redisTemplate.boundHashOps("key2"); //取出age Object age = key2.get("age"); System.out.println(age); //修改age,有该key就会覆盖,没有则会保存 key2.put("age",23); } /** * 二维平面计算距离,要是计算真实的距离推荐三维球面计算真实的距离 */ @Test public void GeoSaveTemplate(){ //插入数据 HashMap<String, org.springframework.data.geo.Point> hashMap = new HashMap<>(); hashMap.put("北京",new org.springframework.data.geo.Point(116.418642D,39.927109D)); hashMap.put("上海",new org.springframework.data.geo.Point(121.477898D,31.26989D)); redisTemplate.opsForGeo().add("key6",hashMap); //计算北京和上海距离,默认返回米,可在最后一个参数指定返回数据的单位 Distance distance = redisTemplate.opsForGeo().distance("key6", "北京", "上海"); System.out.println(distance.toString()); System.out.println(distance.getValue()); }
SpringCache整合Redis
pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
springBootCache 注解介绍
@EnableCaching:用于开启Cache注解功能。
@CacheConfig: 主要用于配置该类中会用到的一些共用的缓存配置
@Cacheable: 主要方法返回值加入缓存。同时在查询时,会先从缓存中取,若不存在才再发起对数据库的访问。
属性介绍:
* value / cacheNames: 指定将方法的返回结果放到那个缓存集中 * key: 就是缓存到redis中key的名字,可以使用 SpEL 表达式的方式来编写 * keyGenerator: key自动生成器,编写配置文件自定义key的生成方法。 * condition: 设置缓存的条件,当条件为true后再缓存redis中 * unless: 与condition相反,当结果为false才会缓存 * sync: false同步缓存 true异步缓存。默认是同步缓存 * cacheManager: 用来指定缓存管理器。 * CacheResolver: 自定义缓存解析器,写配置类需要实现CacheResolver接口@CachePut: 配置于方法上,能够根据参数定义条件进行缓存,与@Cacheable不同的是,每次回真都会添加缓存,所以主要用于数据新增和修改操作上。
属性介绍:等同于Cacheable的属性。
@CacheEvict: 配置于方法上,通常用在删除方法上,用来从缓存中移除对应数据
属性介绍:
* allEntries: true:删除该缓存中的所有key。默认false * beforeInvocation: 1.true:执行方法之前清除缓存,不管代码在执行过程中是否报错,该缓存都将不存在。 2.默认false * 其他等同于Cacheable的属性@Caching:配置于函数上,组合多个Cache注解使用。
Key属性的SpEL 表达式
名字 描述 实例 methodName 当前被调用的方法的名字 #root.methodName method 当前被调用的方法 #root.method.xxx target 当前被调用的目标对象 #root.target targetClass 当前被调用的目标对象类 #root.targetClass args 当前被调用的方法的参数列表 #root.args[0] aches 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”,“cache2”})),则有两个cache #root.caches[0].name result 方法执行后的返回值
(仅当方法执行之后的判断有效,如’unless’、'cache put’的表达式 'cacheevict’的表达式beforeInvocation=false)
#result argument name 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引; #id、#p0、#a0 cacheManager属性指定的缓冲管理器
SimpleCacheManager 使用简单的Collection来存储缓存,主要用于测试 ConcurrentMapCacheManager 使用ConcurrentMap作为缓存技术(默认) NoOpCacheManager 测试用 EhCacheCacheManager 使用EhCache作为缓存技术,以前在hibernate的时候经常用 GuavaCacheManager 使用google guava的GuavaCache作为缓存技术 HazelcastCacheManager 使用Hazelcast作为缓存技术 JCacheCacheManager 使用JCache标准的实现作为缓存技术,如Apache Commons JCS RedisCacheManager 使用Redis作为缓存技术
java代码案例
1.开启springCache注解支持
2.自定义Cache的序列化方式(默认存入道redis中是二进制格式)
@Configuration public class CacheConfig { //第二种、因为注入了容器,参数属性spring会自己去容器里面找 (CacheProperties cacheProperties) @Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){ //FastJsonRedisSerializer序列化方式 RedisSerializer serializer = new FastJsonRedisSerializer(Object.class); CacheProperties.Redis redisProperties = cacheProperties.getRedis(); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(serializer)); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; }
3.测试
/** * condition: 指定id大于10才缓存 * 将所有缓存都放到test:save,指定key为id的实参 */ @GetMapping("/saveTest") @Cacheable(value = "test:save",key = "#id",condition = "#id > 10") public Result cacheSaveTest(Long id){ return new Result().ok("访问成功"); } /** * 更新缓存,key使用了SpEL表达式,更新缓存集中key为11的缓存 */ @PutMapping("/updateTest") @CachePut(value = "test:save",key="#result") public Long updateTest(){ return 11L; } /** * condition: 指定id大于100才删除缓存 * allEntries:指定删除 test:save缓存集中的所有缓存 */ @DeleteMapping("/delTest") @CacheEvict(value = "test:save",allEntries = true,condition = "#id > 100") public Result cacheDeleteTest(Long id){ return new Result().ok("删除成功"); } /** * Caching注解,组合多个注解使用 * @return */ @Caching( cacheable = @Cacheable(value = "test:save",key = "#id",condition = "#id > 10"), put = @CachePut(value = "test:save",key="#result"), evict = @CacheEvict(value = "test:save",allEntries = true,condition = "#id > 100")) public String caching() { return "caching"; }
Redis的事务
1. redis没有事务, 是由Multi、Exec、discard 这三个原语来实现事务的效果
- 输入multi 命令开始事务.其后所有的命令都会放到命令队列中,但不会执行.组队过程中当加入有某个命令写错了.redis会取消整个命令队列.
- 输入exec 后, 开始执行命令队列中的所有命令,执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚
- 输入discard 放弃组队.
2. 在执行multi之前,先执行 WATCH key [key ...] 可以监视一个(或多个) key ,如果在事务执行之前这些 key 被其他命令所改动,那么事务将被打断。
- WATCH key [key ...]: 监听多个key
- unwatch: 取消对key的监听
3. Redis事务三特性
- 单独的隔离操作, 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念, 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性, 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
案例
代码实现:
@Autowired private StringRedisTemplate stringRedisTemplate; @Test public void redisTest(){ //监听key stringRedisTemplate.watch(Arrays.asList("key1","key2","key3")); //开启一个命令队列 stringRedisTemplate.multi(); //...业务逻辑 //执行命令队列中的命令 stringRedisTemplate.exec(); //放弃监听 stringRedisTemplate.unwatch(); }
6. Redis主从复制,集群
6.2 薪火相传
上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。
中途变更转向:会清除之前的数据,重新建立拷贝最新的风险是一旦某个slave宕机,后面的slave都没法备份主机挂了,从机还是从机,无法写数据了
6.3 复制原理
- Slave启动成功连接到master后会发送一个sync命令
- Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
- 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
6.4 哨兵模式
哨兵模式可以监听Redis的master的状态,当master宕机或者出现其他导致master不能运行,哨兵会根据条件选举出一个slave节点,升级为master节点.
哨兵判断redis下线的原理
- Sentinel集群的每一个Sentinel节点会定时对redis集群的所有节点发心跳包检测节点是否正常。如果一个节点在
down-after-milliseconds
时间内没有回复Sentinel节点的心跳包,则该redis节点被该Sentinel节点主观下线。- 当节点被一个Sentinel节点记为主观下线时,并不意味着该节点肯定故障了,还需要Sentinel集群的其他Sentinel节点共同判断为主观下线才行。
- 该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过
quorum
数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线。- 如果客观下线的redis节点是从节点或者是Sentinel节点,则操作到此为止,没有后续的操作了;如果客观下线的redis节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点。
注意: 这个配置是sentinel.conf中的不是redis.conf中的
哨兵自动升级的流程:
- sentinel会从当前所有从节点中根据(优先级高的. 偏移量最大的.runid最小的) 这三个条件选举出一个slave节点,升级为master节点.
- 选举出新的master节点后,sentinel会向所有的slave节点发送slaveof命令,告诉所有的slave节点这是你们新的主节点.
- 当掉线的master节点在上线的时候,sentinel也会向他发送salveof命令让他成为slave节点
docker搭建redis相关模式:
docker-compose 搭建redis哨兵和集群。
springboot集成Redis哨兵
jedis连接池的依赖
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> jedis依赖非必要引入,使用jedis链接池,或者使用jedis客户端操作redis再引入上面的依赖
/** * 两种配置方式,如果使用jedis操作redis就先引入上面的pom依赖,在配置第一个 * 如果我们使用RedisTemplate操作redis直接配置第二个即可 **/ //Jedis操作工具配置哨兵 @Bean public Jedis jedis() { //sentinel 节点的地址 HashSet<String> hashSet = new HashSet<>(); hashSet.add("192.168.100.129:26379"); //配置连接池 JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxIdle(8); //jedis连接池最大连接数 poolConfig.setMinIdle(2); //最小连接数 poolConfig.setBlockWhenExhausted(true); poolConfig.setMaxWaitMillis(3000); //获取连接等待时间 poolConfig.setTestOnBorrow(true); //每次获取都测试下是否可用 JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("rmaster",hashSet,poolConfig); return jedisSentinelPool.getResource(); } //redisTemplate 配置redis的哨兵 @Bean public RedisSentinelConfiguration redisSentinelConfiguration(){ RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration(); //配置matser的名称 redisSentinelConfiguration.master("rmaster"); //配置redis的哨兵sentinel的地址 Set<RedisNode> redisNodeSet = new HashSet<>(); redisNodeSet.add(new RedisNode("192.168.100.129",26379)); //哨兵模式添加set redisSentinelConfiguration.setSentinels(redisNodeSet); return redisSentinelConfiguration; }
springboot集成Redis集群
pom依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
yml配置
spring: redis: cluster: nodes: #下面每个节点的ip一定要是你redis.conf配置中设置的节点ip - 192.168.136.128:6379 - 192.168.136.128:6380 - 192.168.136.128:6381 - 192.168.136.128:6382 - 192.168.136.128:6383 - 192.168.136.128:6384 max-redirects: 3 lettuce: #Redis连接池 pool: max-idle: 16 max-active: 32 min-idle: 8
java测试:
// 其余和单机模式操作一样,使用redisTemplate操作。
@Autowired
private RedisTemplate redisTemplate;
@Test
public void clusterTest(){
redisTemplate.opsForValue().set("测试","测试数据");
Object test = redisTemplate.opsForValue().get("测试");
//集群模式下批量保存会报错
HashMap<String, Object> map = new HashMap<>();
map.put("key1",12);
map.put("key2",120);
map.put("key3",1200);
redisTemplate.opsForValue().multiSet(map);
}
保存到redis中的数据
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)