当前位置: 首页 > news >正文

信誉好的镇江网站建设网站公司深圳

信誉好的镇江网站建设,网站公司深圳,电话营销,商城网站建设企业一、Nosql概述 为什么使用Nosql 1、单机Mysql时代 90年代,一个网站的访问量一般不会太大#xff0c;单个数据库完全够用。随着用户增多#xff0c;网站出现以下问题 数据量增加到一定程度#xff0c;单机数据库就放不下了数据的索引#xff08;B Tree#xff09;,一个机…一、Nosql概述 为什么使用Nosql 1、单机Mysql时代 90年代,一个网站的访问量一般不会太大单个数据库完全够用。随着用户增多网站出现以下问题 数据量增加到一定程度单机数据库就放不下了数据的索引B Tree,一个机器内存也存放不下访问量变大后读写混合一台服务器承受不住。 2、Memcached(缓存) Mysql 垂直拆分读写分离 网站80%的情况都是在读每次都要去查询数据库的话就十分的麻烦所以说我们希望减轻数据库的压力我们可以使用缓存来保证效率 优化过程经历了以下几个过程 优化数据库的数据结构和索引(难度大)文件缓存通过IO流获取比每次都访问数据库效率略高但是流量爆炸式增长时候IO流也承受不了MemCache,当时最热门的技术通过在数据库和数据库访问层之间加上一层缓存第一次访问时查询数据库将结果保存到缓存后续的查询先检查缓存若有直接拿去使用效率显著提升。 3、分库分表 水平拆分 Mysql集群 4、如今最近的年代 如今信息量井喷式增长各种各样的数据出现用户定位数据图片数据等大数据的背景下关系型数据库RDBMS无法满足大量数据要求。Nosql数据库就能轻松解决这些问题。 目前一个基本的互联网项目 为什么要用NoSQL 用户的个人信息社交网络地理位置。用户自己产生的数据用户日志等等爆发式增长 这时候我们就需要使用NoSQL数据库的Nosql可以很好的处理以上的情况 什么是Nosql NoSQL Not Only SQL不仅仅是SQL Not Only Structured Query Language 关系型数据库列行同一个表下数据的结构是一样的。 非关系型数据库数据存储没有固定的格式并且可以进行横向扩展。 NoSQL泛指非关系型数据库随着web2.0互联网的诞生传统的关系型数据库很难对付web2.0时代尤其是超大规模的高并发的社区暴露出来很多难以克服的问题NoSQL在当今大数据环境下发展的十分迅速Redis是发展最快的。 Nosql特点 方便扩展数据之间没有关系很好扩展 大数据量高性能Redis一秒可以写8万次读11万次NoSQL的缓存记录级是一种细粒度的缓存性能会比较高 数据类型是多样型的不需要事先设计数据库随取随用 传统的 RDBMS 和 NoSQL 传统的 RDBMS(关系型数据库) - 结构化组织 - SQL - 数据和关系都存在单独的表中 row col - 操作数据定义语言 - 严格的一致性 - 基础的事务 - ...Nosql - 不仅仅是数据 - 没有固定的查询语言 - 键值对存储列存储文档存储图形数据库社交关系 - 最终一致性 - CAP定理和BASE - 高性能高可用高扩展 - ...了解3V 3高 大数据时代的3V 主要是描述问题的 海量Velume多样Variety实时Velocity 大数据时代的3高 主要是对程序的要求 高并发高可扩高性能 真正在公司中的实践NoSQL RDBMS 一起使用才是最强的。 阿里巴巴演进分析 推荐阅读阿里云的这群疯子https://yq.aliyun.com/articles/653511 # 商品信息 - 一般存放在关系型数据库Mysql,阿里巴巴使用的Mysql都是经过内部改动的。# 商品描述、评论(文字居多) - 文档型数据库MongoDB# 图片 - 分布式文件系统 FastDFS - 淘宝TFS - Google: GFS - Hadoop: HDFS - 阿里云: oss# 商品关键字 用于搜索 - 搜索引擎solr,elasticsearch - 阿里Isearch 多隆# 商品热门的波段信息 - 内存数据库RedisMemcache# 商品交易外部支付接口 - 第三方应用Nosql的四大分类 KV键值对 新浪Redis美团Redis Tair阿里、百度Redis Memcache 文档型数据库bson数据格式 MongoDB(掌握) 基于分布式文件存储的数据库。C编写用于处理大量文档。MongoDB是RDBMS和NoSQL的中间产品。MongoDB是非关系型数据库中功能最丰富的NoSQL中最像关系型数据库的数据库。 ConthDB 列存储数据库 HBase(大数据必学)分布式文件系统 图关系数据库 用于广告推荐社交网络 Neo4j、InfoGrid 分类Examples举例典型应用场景数据模型优点缺点键值对key-valueTokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB内容缓存主要用于处理大量数据的高访问负载也用于一些日志系统等等。Key 指向 Value 的键值对通常用hash table来实现查找速度快数据无结构化通常只被当作字符串或者二进制数据列存储数据库Cassandra, HBase, Riak分布式的文件系统以列簇式存储将同一列数据存在一起查找速度快可扩展性强更容易进行分布式扩展功能相对局限文档型数据库CouchDB, MongoDbWeb应用与Key-Value类似Value是结构化的不同的是数据库能够了解Value的内容Key-Value对应的键值对Value为结构化数据数据结构要求不严格表结构可变不需要像关系型数据库一样需要预先定义表结构查询性能不高而且缺乏统一的查询语法。图形(Graph)数据库Neo4J, InfoGrid, Infinite Graph社交网络推荐系统等。专注于构建关系图谱图结构利用图结构相关算法。比如最短路径寻址N度关系查找等很多时候需要对整个图做计算才能得出需要的信息而且这种结构不太好做分布式的集群 二、Redis入门 概述 Redis是什么 RedisRemote Dictionary Server )即远程字典服务。 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库并提供多种语言的API。 与memcached一样为了保证效率数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件并且在此基础上实现了master-slave(主从)同步。 Redis能该干什么 内存存储、持久化内存是断电即失的所以需要持久化RDB、AOF高效率、用于高速缓冲发布订阅系统地图信息分析计时器、计数器(eg浏览量)。。。 特性 多样的数据类型 持久化 集群 事务 … 环境搭建 官网https://redis.io/ 推荐使用Linux服务器学习。 windows版本的Redis已经停更很久了… Windows安装 https://github.com/dmajkic/redis 解压安装包 开启redis-server.exe启动redis-cli.exe测试 Linux安装 下载安装包redis-5.0.8.tar.gz 解压Redis的安装包解压后程序一般放在 /opt 目录下 tar -zxvf 压缩包名 基本环境安装 yum install gcc-c # 然后进入redis目录下执行 make #寻找本地的makefile文件,自动编辑 # 然后执行 make install #安装编译后的程序redis默认安装路径 /usr/local/bin 将redis的配置文件复制到 程序安装目录 /usr/local/bin/kconfig下 redis默认不是后台启动的需要修改配置文件 通过制定的配置文件启动redis服务 使用redis-cli连接指定的端口号测试Redis-server的默认端口是6379,所以下面客户端选择连接服务的的6379端口。 查看redis进程是否开启 关闭Redis服务 shutdown 再次查看进程是否存在 后面我们会使用单机多Redis启动集群测试 测试性能 redis-benchmarkRedis官方提供的性能测试工具参数如下 简单测试 # 测试100个并发连接 100000请求 redis-benchmark -h localhost -p 6379 -c 100 -n 100000基础知识 redis默认有16个数据库,通过查看其配置文件的默认内容可知。 默认使用的第0个; 16个数据库为DB 0 ~ DB 15 默认使用DB 0 可以使用select n切换到DB ndbsize可以查看当前数据库的大小数据库的大小与其中存储的key数量相关。 127.0.0.1:6379 config get databases # 命令行查看数据库数量databases 1) databases 2) 16127.0.0.1:6379 select 8 # 切换数据库 DB 8 OK 127.0.0.1:6379[8] dbsize # 查看数据库大小 (integer) 0# 不同数据库之间 数据是不能互通的并且dbsize 是根据库中key的个数。 127.0.0.1:6379 set name sakura OK 127.0.0.1:6379 SELECT 8 OK 127.0.0.1:6379[8] get name # db8中并不能获取db0中的键值对。 (nil) 127.0.0.1:6379[8] DBSIZE (integer) 0 127.0.0.1:6379[8] SELECT 0 OK 127.0.0.1:6379 keys * 1) counter:__rand_int__ 2) mylist 3) name 4) key:__rand_int__ 5) myset:__rand_int__ 127.0.0.1:6379 DBSIZE # size和key个数相关 (integer) 5keys * 查看当前数据库中所有的key。 flushdb清空当前数据库中的键值对。 flushall清空所有数据库的键值对。 Redis是单线程的Redis是基于内存操作的。 所以Redis的性能瓶颈不是CPU,而是机器内存和网络带宽。 那么为什么Redis的速度如此快呢性能这么高呢QPS达到10W Redis为什么单线程还这么快 误区1高性能的服务器一定是多线程的误区2多线程CPU上下文会切换一定比单线程效率高 核心Redis是将所有的数据放在内存中的所以说使用单线程去操作效率就是最高的多线程CPU上下文会切换耗时的操作对于内存系统来说如果没有上下文切换效率就是最高的多次读写都是在一个CPU上的在内存存储数据情况下单线程就是最佳的方案。 三、五大数据类型 Redis是一个开源BSD许可内存存储的数据结构服务器可用作数据库高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合位图hyperloglogs等数据类型。内置复制、Lua脚本、LRU收回、事务以及不同级别磁盘持久化功能同时通过Redis Sentinel提供高可用通过Redis Cluster提供自动分区。 Redis-key 在redis中无论什么数据类型在数据库中都是以key-value形式保存通过进行对Redis-key的操作来完成对数据库中数据的操作。 下面学习的命令 exists key判断键是否存在del key删除键值对move key db将键值对移动到指定数据库expire key second设置键值对的过期时间type key查看value的数据类型 127.0.0.1:6379 keys * # 查看当前数据库所有key (empty list or set) 127.0.0.1:6379 set name qinjiang # set key OK 127.0.0.1:6379 set age 20 OK 127.0.0.1:6379 keys * 1) age 2) name 127.0.0.1:6379 move age 1 # 将键值对移动到指定数据库 (integer) 1 127.0.0.1:6379 EXISTS age # 判断键是否存在 (integer) 0 # 不存在 127.0.0.1:6379 EXISTS name (integer) 1 # 存在 127.0.0.1:6379 SELECT 1 OK 127.0.0.1:6379[1] keys * 1) age 127.0.0.1:6379[1] del age # 删除键值对 (integer) 1 # 删除个数127.0.0.1:6379 set age 20 OK 127.0.0.1:6379 EXPIRE age 15 # 设置键值对的过期时间(integer) 1 # 设置成功 开始计数 127.0.0.1:6379 ttl age # 查看key的过期剩余时间 (integer) 13 127.0.0.1:6379 ttl age (integer) 11 127.0.0.1:6379 ttl age (integer) 9 127.0.0.1:6379 ttl age (integer) -2 # -2 表示key过期-1表示key未设置过期时间127.0.0.1:6379 get age # 过期的key 会被自动delete (nil) 127.0.0.1:6379 keys * 1) name127.0.0.1:6379 type name # 查看value的数据类型 string关于TTL命令 Redis的key通过TTL命令返回key的过期时间一般来说有3种 当前key没有设置过期时间所以会返回-1.当前key有设置过期时间而且key已经过期所以会返回-2.当前key有设置过期时间且key还没有过期故会返回key的正常剩余时间. 关于重命名RENAME和RENAMENX 其中的NX的对应单词为: not exist RENAME key newkey修改 key 的名称RENAMENX key newkey仅当 newkey 不存在时将 key 改名为 newkey 。 更多命令学习https://www.redis.net.cn/order/ String(字符串) 普通的set、get直接略过。 命令描述示例APPEND key value向指定的key的value后追加字符串127.0.0.1:6379 set msg hello OK 127.0.0.1:6379 append msg world (integer) 11 127.0.0.1:6379 get msg “hello world”DECR/INCR key将指定key的value数值进行1/-1(仅对于数字)127.0.0.1:6379 set age 20 OK 127.0.0.1:6379 incr age (integer) 21 127.0.0.1:6379 decr age (integer) 20INCRBY/DECRBY key n按指定的步长对数值进行加减127.0.0.1:6379 INCRBY age 5 (integer) 25 127.0.0.1:6379 DECRBY age 10 (integer) 15INCRBYFLOAT key n为数值加上浮点型数值127.0.0.1:6379 INCRBYFLOAT age 5.2 “20.2”STRLEN key获取key保存值的字符串长度127.0.0.1:6379 get msg “hello world” 127.0.0.1:6379 STRLEN msg (integer) 11GETRANGE key start end按起止位置获取字符串闭区间起止位置都取127.0.0.1:6379 get msg “hello world” 127.0.0.1:6379 GETRANGE msg 3 9 “lo worl”SETRANGE key offset value用指定的value 替换key中 offset开始的值127.0.0.1:6379 SETRANGE msg 2 hello (integer) 7 127.0.0.1:6379 get msg “tehello”GETSET key value将给定 key 的值设为 value 并返回 key 的旧值(old value)。127.0.0.1:6379 GETSET msg test “hello world”SETNX key value仅当key不存在时进行set127.0.0.1:6379 SETNX msg test (integer) 0 127.0.0.1:6379 SETNX name sakura (integer) 1SETEX key seconds valueset 键值对并设置过期时间127.0.0.1:6379 setex name 10 root OK 127.0.0.1:6379 get name (nil)MSET key1 value1 [key2 value2..]批量set键值对127.0.0.1:6379 MSET k1 v1 k2 v2 k3 v3 OKMSETNX key1 value1 [key2 value2..]批量设置键值对仅当参数中所有的key都不存在时执行127.0.0.1:6379 MSETNX k1 v1 k4 v4 (integer) 0MGET key1 [key2..]批量获取多个key保存的值127.0.0.1:6379 MGET k1 k2 k3 1) “v1” 2) “v2” 3) “v3”PSETEX key milliseconds value和 SETEX[set expire] 命令相似但它以毫秒为单位设置 key 的生存时间getset key value如果不存在值则返回nil如果存在值获取原来的值并设置新的值 String类似的使用场景value除了是字符串还可以是数字用途举例 计数器统计多单位的数量uid:123666follow 0粉丝数对象存储缓存 List(列表) Redis列表是简单的字符串列表按照插入顺序排序。你可以添加一个元素到列表的头部左边或者尾部右边 首先我们列表可以经过规则定义将其变为队列、栈、双端队列等 正如图Redis中List是可以进行双端操作的所以命令也就分为了LXXX和RLLL两类此时L和R分别代表left和right,但有时候L也表示List例如LLEN 命令描述LPUSH/RPUSH key value1[value2..]从左边/右边向列表中PUSH值(一个或者多个)。LRANGE key start end获取list 起止元素索引从左往右 递增LPUSHX/RPUSHX key value向已存在的列名中push值一个或者多个LINSERT key BEFOREAFTER pivot valueLLEN key查看列表长度LINDEX key index通过索引获取列表元素LSET key index value通过索引为元素设值LPOP/RPOP key从最左边/最右边移除值 并返回RPOPLPUSH source destination将列表的尾部(右)最后一个值弹出并返回然后加到另一个列表的头部LTRIM key start end通过下标截取指定范围内的列表【截取后会更改列表的数据】LREM key count valueList中是允许value重复的 count 0从头部开始搜索 然后删除指定的value 至多删除count个 count 0从尾部开始搜索… count 0删除列表中所有的指定value。BLPOP/BRPOP key1[key2] timout移出并获取列表的第一个/最后一个元素 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。BRPOPLPUSH source destination timeout和RPOPLPUSH功能相同如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 ---------------------------LPUSH---RPUSH---LRANGE--------------------------------127.0.0.1:6379 LPUSH mylist k1 # LPUSH mylist{1} (integer) 1 127.0.0.1:6379 LPUSH mylist k2 # LPUSH mylist{2,1} (integer) 2 127.0.0.1:6379 RPUSH mylist k3 # RPUSH mylist{2,1,3} (integer) 3 127.0.0.1:6379 get mylist # 普通的get是无法获取list值的 (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379 LRANGE mylist 0 4 # LRANGE 获取起止位置范围内的元素 1) k2 2) k1 3) k3 127.0.0.1:6379 LRANGE mylist 0 2 1) k2 2) k1 3) k3 127.0.0.1:6379 LRANGE mylist 0 1 1) k2 2) k1 127.0.0.1:6379 LRANGE mylist 0 -1 # 获取全部元素 1) k2 2) k1 3) k3---------------------------LPUSHX---RPUSHX-----------------------------------127.0.0.1:6379 LPUSHX list v1 # list不存在 LPUSHX失败 (integer) 0 127.0.0.1:6379 LPUSHX list v1 v2 (integer) 0 127.0.0.1:6379 LPUSHX mylist k4 k5 # 向mylist中 左边 PUSH k4 k5 (integer) 5 127.0.0.1:6379 LRANGE mylist 0 -1 1) k5 2) k4 3) k2 4) k1 5) k3---------------------------LINSERT--LLEN--LINDEX--LSET----------------------------127.0.0.1:6379 LINSERT mylist after k2 ins_key1 # 在k2元素后 插入ins_key1 (integer) 6 127.0.0.1:6379 LRANGE mylist 0 -1 1) k5 2) k4 3) k2 4) ins_key1 5) k1 6) k3 127.0.0.1:6379 LLEN mylist # 查看mylist的长度 (integer) 6 127.0.0.1:6379 LINDEX mylist 3 # 获取下标为3的元素 ins_key1 127.0.0.1:6379 LINDEX mylist 0 k5 127.0.0.1:6379 LSET mylist 3 k6 # 将下标3的元素 set值为k6 OK 127.0.0.1:6379 LRANGE mylist 0 -1 1) k5 2) k4 3) k2 4) k6 5) k1 6) k3---------------------------LPOP--RPOP--------------------------127.0.0.1:6379 LPOP mylist # 左侧(头部)弹出 k5 127.0.0.1:6379 RPOP mylist # 右侧(尾部)弹出 k3---------------------------RPOPLPUSH--------------------------127.0.0.1:6379 LRANGE mylist 0 -1 1) k4 2) k2 3) k6 4) k1 127.0.0.1:6379 RPOPLPUSH mylist newlist # 将mylist的最后一个值(k1)弹出加入到newlist的头部 k1 127.0.0.1:6379 LRANGE newlist 0 -1 1) k1 127.0.0.1:6379 LRANGE mylist 0 -1 1) k4 2) k2 3) k6---------------------------LTRIM--------------------------127.0.0.1:6379 LTRIM mylist 0 1 # 截取mylist中的 0~1部分 OK 127.0.0.1:6379 LRANGE mylist 0 -1 1) k4 2) k2# 初始 mylist: k2,k2,k2,k2,k2,k2,k4,k2,k2,k2,k2 ---------------------------LREM--------------------------127.0.0.1:6379 LREM mylist 3 k2 # 从头部开始搜索 至多删除3个 k2 (integer) 3 # 删除后mylist: k2,k2,k2,k4,k2,k2,k2,k2127.0.0.1:6379 LREM mylist -2 k2 #从尾部开始搜索 至多删除2个 k2 (integer) 2 # 删除后mylist: k2,k2,k2,k4,k2,k2---------------------------BLPOP--BRPOP--------------------------mylist: k2,k2,k2,k4,k2,k2 newlist: k1127.0.0.1:6379 BLPOP newlist mylist 30 # 从newlist中弹出第一个值mylist作为候选 1) newlist # 弹出 2) k1 127.0.0.1:6379 BLPOP newlist mylist 30 1) mylist # 由于newlist空了 从mylist中弹出 2) k2 127.0.0.1:6379 BLPOP newlist 30 (30.10s) # 超时了127.0.0.1:6379 BLPOP newlist 30 # 我们连接另一个客户端向newlist中push了test, 阻塞被解决。 1) newlist 2) test (12.54s)小结 list实际上是一个链表before Node after , left, right 都可以插入值如果key不存在则创建新的链表如果key存在新增内容如果移除了所有值空链表也代表不存在在两边插入或者改动值效率最高修改中间元素效率相对较低 应用 消息排队消息队列Lpush Rpop,栈Lpush Lpop Set(集合) Redis的Set是string类型的无序集合。集合成员是唯一的这就意味着集合中不能出现重复的数据。 Redis 中 集合是通过哈希表实现的所以添加删除查找的复杂度都是O(1)。 命令描述SADD key member1[member2..]向集合中无序增加一个/多个成员SCARD key获取集合的成员数SMEMBERS key返回集合中所有的成员SISMEMBER key member查询member元素是否是集合的成员,结果是无序的SRANDMEMBER key [count]随机返回集合中count个成员count缺省值为1SPOP key [count]随机移除并返回集合中count个成员count缺省值为1SMOVE source destination member将source集合的成员member移动到destination集合SREM key member1[member2..]移除集合中一个/多个成员SDIFF key1[key2..]返回所有集合的差集 key1- key2 - …SDIFFSTORE destination key1[key2..]在SDIFF的基础上将结果保存到集合中(覆盖)。不能保存到其他类型key噢SINTER key1 [key2..]返回所有集合的交集SINTERSTORE destination key1[key2..]在SINTER的基础上存储结果到集合中。覆盖SUNION key1 [key2..]返回所有集合的并集SUNIONSTORE destination key1 [key2..]在SUNION的基础上存储结果到及和张。覆盖SSCAN KEY [MATCH pattern] [COUNT count]在大量数据环境下使用此命令遍历集合中元素每次遍历部分 ---------------SADD--SCARD--SMEMBERS--SISMEMBER--------------------127.0.0.1:6379 SADD myset m1 m2 m3 m4 # 向myset中增加成员 m1~m4 (integer) 4 127.0.0.1:6379 SCARD myset # 获取集合的成员数目 (integer) 4 127.0.0.1:6379 smembers myset # 获取集合中所有成员 1) m4 2) m3 3) m2 4) m1 127.0.0.1:6379 SISMEMBER myset m5 # 查询m5是否是myset的成员 (integer) 0 # 不是返回0 127.0.0.1:6379 SISMEMBER myset m2 (integer) 1 # 是返回1 127.0.0.1:6379 SISMEMBER myset m3 (integer) 1---------------------SRANDMEMBER--SPOP----------------------------------127.0.0.1:6379 SRANDMEMBER myset 3 # 随机返回3个成员 1) m2 2) m3 3) m4 127.0.0.1:6379 SRANDMEMBER myset # 随机返回1个成员 m3 127.0.0.1:6379 SPOP myset 2 # 随机移除并返回2个成员 1) m1 2) m4 # 将set还原到{m1,m2,m3,m4}---------------------SMOVE--SREM----------------------------------------127.0.0.1:6379 SMOVE myset newset m3 # 将myset中m3成员移动到newset集合 (integer) 1 127.0.0.1:6379 SMEMBERS myset 1) m4 2) m2 3) m1 127.0.0.1:6379 SMEMBERS newset 1) m3 127.0.0.1:6379 SREM newset m3 # 从newset中移除m3元素 (integer) 1 127.0.0.1:6379 SMEMBERS newset (empty list or set)# 下面开始是多集合操作,多集合操作中若只有一个参数默认和自身进行运算 # setx{m1,m2,m4,m6}, sety{m2,m5,m6}, setz{m1,m3,m6}-----------------------------SDIFF------------------------------------127.0.0.1:6379 SDIFF setx sety setz # 等价于setx-sety-setz 1) m4 127.0.0.1:6379 SDIFF setx sety # setx - sety 1) m4 2) m1 127.0.0.1:6379 SDIFF sety setx # sety - setx 1) m5-------------------------SINTER--------------------------------------- # 共同关注交集127.0.0.1:6379 SINTER setx sety setz # 求 setx、sety、setx的交集 1) m6 127.0.0.1:6379 SINTER setx sety # 求setx sety的交集 1) m2 2) m6-------------------------SUNION---------------------------------------127.0.0.1:6379 SUNION setx sety setz # setx sety setz的并集 1) m4 2) m6 3) m3 4) m2 5) m1 6) m5 127.0.0.1:6379 SUNION setx sety # setx sety 并集 1) m4 2) m6 3) m2 4) m1 5) m5Hash哈希 Redis hash 是一个string类型的field和value的映射表hash特别适合用于存储对象。 Set就是一种简化Hash,只变动key,而value使用默认值填充。可以将一个Hash表作为一个对象进行存储表中存放对象的信息。 命令描述HSET key field value将哈希表 key 中的字段 field 的值设为 value 。重复设置同一个field会覆盖,返回0HMSET key field1 value1 [field2 value2..]同时将多个 field-value (域-值)对设置到哈希表 key 中。HSETNX key field value只有在字段 field 不存在时设置哈希表字段的值。HEXISTS key field查看哈希表 key 中指定的字段是否存在。HGET key field value获取存储在哈希表中指定字段的值HMGET key field1 [field2..]获取所有给定字段的值HGETALL key获取在哈希表key 的所有字段和值HKEYS key获取哈希表key中所有的字段HLEN key获取哈希表中字段的数量HVALS key获取哈希表中所有值HDEL key field1 [field2..]删除哈希表key中一个/多个field字段HINCRBY key field n为哈希表 key 中的指定字段的整数值加上增量n并返回增量后结果 一样只适用于整数型字段HINCRBYFLOAT key field n为哈希表 key 中的指定字段的浮点数值加上增量 n。HSCAN key cursor [MATCH pattern] [COUNT count]迭代哈希表中的键值对。 ------------------------HSET--HMSET--HSETNX---------------- 127.0.0.1:6379 HSET studentx name sakura # 将studentx哈希表作为一个对象设置name为sakura (integer) 1 127.0.0.1:6379 HSET studentx name gyc # 重复设置field进行覆盖并返回0 (integer) 0 127.0.0.1:6379 HSET studentx age 20 # 设置studentx的age为20 (integer) 1 127.0.0.1:6379 HMSET studentx sex 1 tel 15623667886 # 设置sex为1tel为15623667886 OK 127.0.0.1:6379 HSETNX studentx name gyc # HSETNX 设置已存在的field (integer) 0 # 失败 127.0.0.1:6379 HSETNX studentx email 12345qq.com (integer) 1 # 成功----------------------HEXISTS-------------------------------- 127.0.0.1:6379 HEXISTS studentx name # name字段在studentx中是否存在 (integer) 1 # 存在 127.0.0.1:6379 HEXISTS studentx addr (integer) 0 # 不存在-------------------HGET--HMGET--HGETALL----------- 127.0.0.1:6379 HGET studentx name # 获取studentx中name字段的value gyc 127.0.0.1:6379 HMGET studentx name age tel # 获取studentx中name、age、tel字段的value 1) gyc 2) 20 3) 15623667886 127.0.0.1:6379 HGETALL studentx # 获取studentx中所有的field及其value1) name2) gyc3) age4) 205) sex6) 17) tel8) 156236678869) email 10) 12345qq.com--------------------HKEYS--HLEN--HVALS-------------- 127.0.0.1:6379 HKEYS studentx # 查看studentx中所有的field 1) name 2) age 3) sex 4) tel 5) email 127.0.0.1:6379 HLEN studentx # 查看studentx中的字段数量 (integer) 5 127.0.0.1:6379 HVALS studentx # 查看studentx中所有的value 1) gyc 2) 20 3) 1 4) 15623667886 5) 12345qq.com-------------------------HDEL-------------------------- 127.0.0.1:6379 HDEL studentx sex tel # 删除studentx 中的sex、tel字段 (integer) 2 127.0.0.1:6379 HKEYS studentx 1) name 2) age 3) email-------------HINCRBY--HINCRBYFLOAT------------------------ 127.0.0.1:6379 HINCRBY studentx age 1 # studentx的age字段数值1 (integer) 21 127.0.0.1:6379 HINCRBY studentx name 1 # 非整数字型字段不可用 (error) ERR hash value is not an integer 127.0.0.1:6379 HINCRBYFLOAT studentx weight 0.6 # weight字段增加0.6 90.8Hash变更的数据user name age尤其是用户信息之类的经常变动的信息Hash更适合于对象的存储Sring更加适合字符串存储 Zset有序集合 不同的是每个元素都会关联一个double类型的分数score。redis正是通过分数来为集合中的成员进行从小到大的排序。 score相同按字典顺序排序 有序集合的成员是唯一的,但分数(score)却可以重复。 命令描述ZADD key score member1 [score2 member2]向有序集合添加一个或多个成员或者更新已存在成员的分数ZCARD key获取有序集合的成员数ZCOUNT key min max计算在有序集合中指定区间score的成员数ZINCRBY key n member有序集合中对指定成员的分数加上增量 nZSCORE key member返回有序集中成员的分数值ZRANK key member返回有序集合中指定成员的索引ZRANGE key start end通过索引区间返回有序集合成指定区间内的成员ZRANGEBYLEX key min max通过字典区间返回有序集合的成员ZRANGEBYSCORE key min max通过分数返回有序集合指定区间内的成员-inf 和 inf分别表示最小最大值只支持开区间()ZLEXCOUNT key min max在有序集合中计算指定字典区间内成员数量ZREM key member1 [member2..]移除有序集合中一个/多个成员ZREMRANGEBYLEX key min max移除有序集合中给定的字典区间的所有成员ZREMRANGEBYRANK key start stop移除有序集合中给定的排名区间的所有成员ZREMRANGEBYSCORE key min max移除有序集合中给定的分数区间的所有成员ZREVRANGE key start end返回有序集中指定区间内的成员通过索引分数从高到底ZREVRANGEBYSCORRE key max min返回有序集中指定分数区间内的成员分数从高到低排序ZREVRANGEBYLEX key max min返回有序集中指定字典区间内的成员按字典顺序倒序ZREVRANK key member返回有序集合中指定成员的排名有序集成员按分数值递减(从大到小)排序ZINTERSTORE destination numkeys key1 [key2 ..]计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中numkeys表示参与运算的集合数将score相加作为结果的scoreZUNIONSTORE destination numkeys key1 [key2..]计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中ZSCAN key cursor [MATCH pattern\] [COUNT count]迭代有序集合中的元素包括元素成员和元素分值 -------------------ZADD--ZCARD--ZCOUNT-------------- 127.0.0.1:6379 ZADD myzset 1 m1 2 m2 3 m3 # 向有序集合myzset中添加成员m1 score1 以及成员m2 score2.. (integer) 2 127.0.0.1:6379 ZCARD myzset # 获取有序集合的成员数 (integer) 2 127.0.0.1:6379 ZCOUNT myzset 0 1 # 获取score在 [0,1]区间的成员数量 (integer) 1 127.0.0.1:6379 ZCOUNT myzset 0 2 (integer) 2----------------ZINCRBY--ZSCORE-------------------------- 127.0.0.1:6379 ZINCRBY myzset 5 m2 # 将成员m2的score 5 7 127.0.0.1:6379 ZSCORE myzset m1 # 获取成员m1的score 1 127.0.0.1:6379 ZSCORE myzset m2 7--------------ZRANK--ZRANGE----------------------------------- 127.0.0.1:6379 ZRANK myzset m1 # 获取成员m1的索引索引按照score排序score相同索引值按字典顺序顺序增加 (integer) 0 127.0.0.1:6379 ZRANK myzset m2 (integer) 2 127.0.0.1:6379 ZRANGE myzset 0 1 # 获取索引在 0~1的成员 1) m1 2) m3 127.0.0.1:6379 ZRANGE myzset 0 -1 # 获取全部成员 1) m1 2) m3 3) m2#testset{abc,add,amaze,apple,back,java,redis} score均为0 ------------------ZRANGEBYLEX--------------------------------- 127.0.0.1:6379 ZRANGEBYLEX testset - # 返回所有成员 1) abc 2) add 3) amaze 4) apple 5) back 6) java 7) redis 127.0.0.1:6379 ZRANGEBYLEX testset - LIMIT 0 3 # 分页 按索引显示查询结果的 0,1,2条记录 1) abc 2) add 3) amaze 127.0.0.1:6379 ZRANGEBYLEX testset - LIMIT 3 3 # 显示 3,4,5条记录 1) apple 2) back 3) java 127.0.0.1:6379 ZRANGEBYLEX testset (- [apple # 显示 (-,apple] 区间内的成员 1) abc 2) add 3) amaze 4) apple 127.0.0.1:6379 ZRANGEBYLEX testset [apple [java # 显示 [apple,java]字典区间的成员 1) apple 2) back 3) java-----------------------ZRANGEBYSCORE--------------------- 127.0.0.1:6379 ZRANGEBYSCORE myzset 1 10 # 返回score在 [1,10]之间的的成员 1) m1 2) m3 3) m2 127.0.0.1:6379 ZRANGEBYSCORE myzset 1 5 1) m1 2) m3--------------------ZLEXCOUNT----------------------------- 127.0.0.1:6379 ZLEXCOUNT testset - (integer) 7 127.0.0.1:6379 ZLEXCOUNT testset [apple [java (integer) 3------------------ZREM--ZREMRANGEBYLEX--ZREMRANGBYRANK--ZREMRANGEBYSCORE-------------------------------- 127.0.0.1:6379 ZREM testset abc # 移除成员abc (integer) 1 127.0.0.1:6379 ZREMRANGEBYLEX testset [apple [java # 移除字典区间[apple,java]中的所有成员 (integer) 3 127.0.0.1:6379 ZREMRANGEBYRANK testset 0 1 # 移除排名0~1的所有成员 (integer) 2 127.0.0.1:6379 ZREMRANGEBYSCORE myzset 0 3 # 移除score在 [0,3]的成员 (integer) 2# testset {abc,add,apple,amaze,back,java,redis} score均为0 # myzset {(m1,1),(m2,2),(m3,3),(m4,4),(m7,7),(m9,9)} ----------------ZREVRANGE--ZREVRANGEBYSCORE--ZREVRANGEBYLEX----------- 127.0.0.1:6379 ZREVRANGE myzset 0 3 # 按score递减排序然后按索引返回结果的 0~3 1) m9 2) m7 3) m4 4) m3 127.0.0.1:6379 ZREVRANGE myzset 2 4 # 返回排序结果的 索引的2~4 1) m4 2) m3 3) m2 127.0.0.1:6379 ZREVRANGEBYSCORE myzset 6 2 # 按score递减顺序 返回集合中分数在[2,6]之间的成员 1) m4 2) m3 3) m2 127.0.0.1:6379 ZREVRANGEBYLEX testset [java (add # 按字典倒序 返回集合中(add,java]字典区间的成员 1) java 2) back 3) apple 4) amaze-------------------------ZREVRANK------------------------------ 127.0.0.1:6379 ZREVRANK myzset m7 # 按score递减顺序返回成员m7索引 (integer) 1 127.0.0.1:6379 ZREVRANK myzset m2 (integer) 4# mathscore{(xm,90),(xh,95),(xg,87)} 小明、小红、小刚的数学成绩 # enscore{(xm,70),(xh,93),(xg,90)} 小明、小红、小刚的英语成绩 -------------------ZINTERSTORE--ZUNIONSTORE----------------------------------- 127.0.0.1:6379 ZINTERSTORE sumscore 2 mathscore enscore # 将mathscore enscore进行合并 结果存放到sumscore (integer) 3 127.0.0.1:6379 ZRANGE sumscore 0 -1 withscores # 合并后的score是之前集合中所有score的和 1) xm 2) 160 3) xg 4) 177 5) xh 6) 188127.0.0.1:6379 ZUNIONSTORE lowestscore 2 mathscore enscore AGGREGATE MIN # 取两个集合的成员score最小值作为结果的 (integer) 3 127.0.0.1:6379 ZRANGE lowestscore 0 -1 withscores 1) xm 2) 70 3) xg 4) 87 5) xh 6) 93应用案例 set排序 存储班级成绩表 工资表排序普通消息1.重要消息 2.带权重进行判断排行榜应用实现取Top N测试 四、三种特殊数据类型 Geospatial(地理位置) 使用经纬度定位地理坐标并用一个有序集合zset保存所以zset命令也可以使用 命令描述geoadd key longitud(经度) latitude(纬度) member [..]将具体经纬度的坐标存入一个有序集合geopos key member [member..]获取集合中的一个/多个成员坐标geodist key member1 member2 [unit]返回两个给定位置之间的距离。默认以米作为单位。georadius key longitude latitude radius mkmGEORADIUSBYMEMBER key member radius...功能与GEORADIUS相同只是中心位置不是具体的经纬度而是使用结合中已有的成员作为中心点。geohash key member1 [member2..]返回一个或多个位置元素的Geohash表示。使用Geohash位置52点整数编码。 有效经纬度 有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。 指定单位的参数 unit 必须是以下单位的其中一个 m 表示单位为米。km 表示单位为千米。mi 表示单位为英里。ft 表示单位为英尺。 关于GEORADIUS的参数 通过georadius就可以完成 附近的人功能 withcoord:带上坐标 withdist:带上距离单位与半径单位相同 COUNT n : 只显示前n个(按距离递增排序) ----------------georadius--------------------- 127.0.0.1:6379 GEORADIUS china:city 120 30 500 km withcoord withdist # 查询经纬度(120,30)坐标500km半径内的成员 1) 1) hangzhou2) 29.41513) 1) 120.200002491474152) 30.199999888333501 2) 1) shanghai2) 205.36113) 1) 121.400001347064972) 31.400000253193539------------geohash--------------------------- 127.0.0.1:6379 geohash china:city yichang shanghai # 获取成员经纬坐标的geohash表示 1) wmrjwbr5250 2) wtw6ds0y300Hyperloglog(基数统计) Redis HyperLogLog 是用来做基数统计的算法HyperLogLog 的优点是在输入元素的数量或者体积非常非常大时计算基数所需的空间总是固定的、并且是很小的。 花费 12 KB 内存就可以计算接近 2^64 个不同元素的基数。 因为 HyperLogLog 只会根据输入元素来计算基数而不会储存输入元素本身所以 HyperLogLog 不能像集合那样返回输入的各个元素。 其底层使用string数据类型 什么是基数 数据集中不重复的元素的个数。 应用场景 网页的访问量UV一个用户多次访问也只能算作一个人。 传统实现存储用户的id,然后每次进行比较。当用户变多之后这种方式及其浪费空间而我们的目的只是计数Hyperloglog就能帮助我们利用最小的空间完成。 命令描述PFADD key element1 [elememt2..]添加指定元素到 HyperLogLog 中PFCOUNT key [key]返回给定 HyperLogLog 的基数估算值。PFMERGE destkey sourcekey [sourcekey..]将多个 HyperLogLog 合并为一个 HyperLogLog ----------PFADD--PFCOUNT--------------------- 127.0.0.1:6379 PFADD myelemx a b c d e f g h i j k # 添加元素 (integer) 1 127.0.0.1:6379 type myelemx # hyperloglog底层使用String string 127.0.0.1:6379 PFCOUNT myelemx # 估算myelemx的基数 (integer) 11 127.0.0.1:6379 PFADD myelemy i j k z m c b v p q s (integer) 1 127.0.0.1:6379 PFCOUNT myelemy (integer) 11----------------PFMERGE----------------------- 127.0.0.1:6379 PFMERGE myelemz myelemx myelemy # 合并myelemx和myelemy 成为myelemz OK 127.0.0.1:6379 PFCOUNT myelemz # 估算基数 (integer) 17如果允许容错那么一定可以使用Hyperloglog ! 如果不允许容错就使用set或者自己的数据类型即可 BitMaps(位图) 使用位存储信息状态只有 0 和 1 Bitmap是一串连续的2进制数字0或1每一位所在的位置为偏移(offset)在bitmap上可执行AND,OR,XOR,NOT以及其它位操作。 应用场景 签到统计、状态统计 命令描述setbit key offset value为指定key的offset位设置值getbit key offset获取offset位的值bitcount key [start end]统计字符串被设置为1的bit数也可以指定统计范围按字节bitop operration destkey key[key..]对一个或多个保存二进制位的字符串 key 进行位元操作并将结果保存到 destkey 上。BITPOS key bit [start] [end]返回字符串里面第一个被设置为1或者0的bit位。start和end只能按字节,不能按位 ------------setbit--getbit-------------- 127.0.0.1:6379 setbit sign 0 1 # 设置sign的第0位为 1 (integer) 0 127.0.0.1:6379 setbit sign 2 1 # 设置sign的第2位为 1 不设置默认 是0 (integer) 0 127.0.0.1:6379 setbit sign 3 1 (integer) 0 127.0.0.1:6379 setbit sign 5 1 (integer) 0 127.0.0.1:6379 type sign string127.0.0.1:6379 getbit sign 2 # 获取第2位的数值 (integer) 1 127.0.0.1:6379 getbit sign 3 (integer) 1 127.0.0.1:6379 getbit sign 4 # 未设置默认是0 (integer) 0-----------bitcount---------------------------- 127.0.0.1:6379 BITCOUNT sign # 统计sign中为1的位数 (integer) 4bitmaps的底层 bitmaps是一串从左到右的二进制串。 五、事务 Redis的单条命令是保证原子性的但是redis事务不能保证原子性 Redis事务本质一组命令的集合。 ----------------- 队列 set set set 执行 ------------------- 事务中每条命令都会被序列化执行过程中按顺序执行不允许其他命令进行干扰。 一次性顺序性排他性 Redis事务没有隔离级别的概念Redis单条命令是保证原子性的但是事务不保证原子性 Redis事务操作过程 开启事务multi命令入队执行事务exec 所以事务中的命令在加入时都没有被执行直到提交时才会开始执行(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 get k1 QUEUED 127.0.0.1:6379 set k3 v3 QUEUED 127.0.0.1:6379 keys * QUEUED 127.0.0.1:6379 exec # 事务执行 1) OK 2) OK 3) v1 4) OK 5) 1) k32) k23) k1取消事务(discurd) 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 DISCARD # 放弃事务 OK 127.0.0.1:6379 EXEC (error) ERR EXEC without MULTI # 当前未开启事务 127.0.0.1:6379 get k1 # 被放弃事务中命令并未执行 (nil)事务错误 代码语法错误编译时异常所有的命令都不执行 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 error k1 # 这是一条语法错误命令 (error) ERR unknown command error, with args beginning with: k1, # 会报错但是不影响后续命令入队 127.0.0.1:6379 get k2 QUEUED 127.0.0.1:6379 EXEC (error) EXECABORT Transaction discarded because of previous errors. # 执行报错 127.0.0.1:6379 get k1 (nil) # 其他命令并没有被执行代码逻辑错误 (运行时异常) **其他命令可以正常执行 ** 所以不保证事务原子性 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 INCR k1 # 这条命令逻辑错误对字符串进行增量 QUEUED 127.0.0.1:6379 get k2 QUEUED 127.0.0.1:6379 exec 1) OK 2) OK 3) (error) ERR value is not an integer or out of range # 运行时报错 4) v2 # 其他命令正常执行# 虽然中间有一条命令报错了但是后面的指令依旧正常执行成功了。 # 所以说Redis单条指令保证原子性但是Redis事务不能保证原子性。监控 悲观锁 很悲观认为什么时候都会出现问题无论做什么都会加锁 乐观锁 很乐观认为什么时候都不会出现问题所以不会上锁更新数据的时候去判断一下在此期间是否有人修改过这个数据获取version更新的时候比较version 使用watch key监控指定数据相当于乐观锁加锁。 正常执行 127.0.0.1:6379 set money 100 # 设置余额:100 OK 127.0.0.1:6379 set use 0 # 支出使用:0 OK 127.0.0.1:6379 watch money # 监视money (上锁) OK 127.0.0.1:6379 multi OK 127.0.0.1:6379 DECRBY money 20 QUEUED 127.0.0.1:6379 INCRBY use 20 QUEUED 127.0.0.1:6379 exec # 监视值没有被中途修改事务正常执行 1) (integer) 80 2) (integer) 20测试多线程修改值使用watch可以当做redis的乐观锁操作相当于getversion 我们启动另外一个客户端模拟插队线程。 线程1 127.0.0.1:6379 watch money # money上锁 OK 127.0.0.1:6379 multi OK 127.0.0.1:6379 DECRBY money 20 QUEUED 127.0.0.1:6379 INCRBY use 20 QUEUED 127.0.0.1:6379 # 此时事务并没有执行模拟线程插队线程2 127.0.0.1:6379 INCRBY money 500 # 修改了线程一中监视的money (integer) 600回到线程1执行事务 127.0.0.1:6379 EXEC # 执行之前另一个线程修改了我们的值这个时候就会导致事务执行失败 (nil) # 没有结果说明事务执行失败127.0.0.1:6379 get money # 线程2 修改生效 600 127.0.0.1:6379 get use # 线程1事务执行失败数值没有被修改 0如果修改失败,获取最新的值就好 解锁获取最新值然后再加锁进行事务。 unwatch进行解锁。 注意每次提交执行exec后都会自动释放锁不管是否成功 六、Jedis 使用Java来操作RedisJedis是Redis官方推荐使用的Java连接redis的客户端。 导入依赖 !--导入jredis的包-- dependencygroupIdredis.clients/groupIdartifactIdjedis/artifactIdversion3.2.0/version /dependency !--fastjson, 处理json-- dependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion1.2.70/version /dependency编码测试 连接数据库 修改redis的配置文件redis.conf vim /usr/local/bin/myconfig/redis.conf 1将只绑定本地更改掉 保护模式改为 no 允许后台运行 开放端口6379 firewall-cmd --zonepublic --add-port6379/tcp --permanent 1重启防火墙服务 systemctl restart firewalld.service阿里云服务器控制台配置安全组 以修改后的配置文件来重启redis-server [rootAlibabaECS bin]# redis-server myconfig/redis.conf 1测试的脚本: TestPing.java public class TestPing {public static void main(String[] args) {Jedis jedis new Jedis(192.168.xx.xxx, 6379);String response jedis.ping();System.out.println(response); // PONG} }输出: java中使用redis事务的测试脚本示例: java中使用的所有的api命令,就是我们上面学习的redis的指令,一个都没有变化!! public class TestTX {public static void main(String[] args) {Jedis jedis new Jedis(39.99.xxx.xx, 6379);JSONObject jsonObject new JSONObject();jsonObject.put(hello, world);jsonObject.put(name, kuangshen);// 开启事务Transaction multi jedis.multi();String result jsonObject.toJSONString();// jedis.watch(result)try {multi.set(user1, result);multi.set(user2, result);// 执行事务multi.exec();}catch (Exception e){// 放弃事务multi.discard();} finally {// 关闭连接System.out.println(jedis.get(user1));System.out.println(jedis.get(user2));jedis.close();}} }七、SpringBoot整合 导入依赖 dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId /dependencyspringboot 2.x后 原来使用的 Jedis 被 lettuce 替换。 jedis采用的直连多个线程操作的话是不安全的。如果要避免不安全使用jedis pool连接池但是更像BIO模式 lettuce采用netty实例可以在多个线程中共享不存在线程不安全的情况可以减少线程数据了更像NIO模式 我们在学习SpringBoot自动配置的原理时整合一个组件并进行配置一定会有一个自动配置类xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名。Redis也不例外,如下 那么就一定还存在一个RedisProperties类 之前我们说SpringBoot2.x后默认使用Lettuce来替换Jedis现在我们就能来验证了。 先看Jedis: ConditionalOnClass注解中有两个类爆红是默认不存在的所以Jedis是无法生效的 然后再看Lettuce 完美生效。 现在我们回到RedisAutoConfiguration 只有两个简单的Bean RedisTemplate StringRedisTemplate [由于String是redis种最常使用的类型,所以单独提出来了一个bean] 当看到xxTemplate时可以对比RestTemplat、SqlSessionTemplate,一般我们通过使用这些xxTemplate来间接操作组件。那么这俩也不会例外。分别用于操作Redis和Redis中的String数据类型。 在RedisTemplate上也有一个条件注解说明我们是可以对其进行定制化的 说完这些我们需要知道如何编写配置文件然后连接Redis就需要阅读RedisProperties,如下: 这是一些基本的配置属性。 还有一些连接池相关的配置。注意使用时一定使用Lettuce的连接池。 编写配置文件 # 配置redis spring.redis.host39.99.xxx.xx spring.redis.port6379使用RedisTemplate SpringBootTest class Redis02SpringbootApplicationTests {Autowiredprivate RedisTemplate redisTemplate;Testvoid contextLoads() {// redisTemplate 操作不同的数据类型api和我们的redis操作指令是一样的// opsForValue 操作字符串 类似String// opsForList 操作List 类似List// opsForHash// opsForZSet// opsForGeo// opsForHyperLogLog// 除了基本的操作我们常用的方法都可以直接通过redisTemplate操作比如事务和基本的CRUD// 获取连接对象//RedisConnection connection redisTemplate.getConnectionFactory().getConnection();//connection.flushDb();//connection.flushAll();redisTemplate.opsForValue().set(mykey,kuangshen);System.out.println(redisTemplate.opsForValue().get(mykey));} }测试结果 此时我们回到Redis查看数据时候惊奇发现全是乱码可是程序中可以正常打印输出 这时候就关系到存储对象的序列化问题在网络中传输的对象也是一样需要序列化否则就全是乱码。 我们转到看那个默认的RedisTemplate内部什么样子 如上图所示,在最开始就能看到几个关于序列化的参数。 默认的序列化器是采用JDK序列化器 故默认的RedisTemplate中的所有序列化器都是使用这个序列化器 后续我们定制自己的RedisTemplate就可以对其进行修改。 RedisSerializer提供了多种序列化方案 直接调用RedisSerializer的静态方法来返回序列化器然后set 自己new 相应的实现类然后set 定制RedisTemplate的模板 我们创建一个Bean组件加入spring容器就会触发RedisTemplate上的条件注解使默认的RedisTemplate失效。 //这是一个固定的模板,企业中可以直接使用 //自己定义了一个RedisTemplate Configuration public class RedisConfig {//为了自己开发方便,一般直接使用String,ObjectBeanpublic RedisTemplateString, Object redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {// 将template 泛型设置为 String, ObjectRedisTemplateString, Object template new RedisTemplate();// 连接工厂不必修改template.setConnectionFactory(redisConnectionFactory);/** 序列化设置*/// key、hash的key 采用 String序列化方式template.setKeySerializer(RedisSerializer.string());template.setHashKeySerializer(RedisSerializer.string());// value、hash的value 采用 Jackson 序列化方式template.setValueSerializer(RedisSerializer.json());template.setHashValueSerializer(RedisSerializer.json());template.afterPropertiesSet();return template;} }这样一来只要实体类进行了序列化我们存什么都不会有乱码的担忧了。 所有的redis操作,重要的是去理解redis的思想和每种数据结构的用处和应用场景。 八、自定义Redis工具类 使用RedisTemplate需要频繁调用.opForxxx然后才能进行对应的操作这样使用起来代码效率低下工作中一般不会这样使用而是将这些常用的公共API抽取出来封装成为一个工具类然后直接使用工具类来间接操作Redis,不但效率高并且易用。 工具类参考博客 https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html https://www.cnblogs.com/zhzhlong/p/11434284.html 九、Redis.conf配置文件详解 redis启动的时候,就通过配置文件来启动! 工作中,一些小小的配置,可以让你脱颖而出。 容量单位不区分大小写G和GB有区别 可以使用 include 组合多个配置文件 网络配置 日志输出级别 日志输出文件 守护进程 daemonize yes #以守护进程的方式运行,默认是no,需要自己开启其它 databases 16 #数据库的熟练,默认是16个数据库 always-show-logo yes #是否总是显示logo持久化规则 由于Redis是基于内存的数据库断电即失,需要将数据由内存持久化到文件中. 持久化:在规定的时间内,执行了多少次操作,则会持久化到文件中。 持久化方式 RDB(Redis DataBase)AOF(Append Only File) RDB文件相关 主从复制 Security模块中进行安全密码设置,redis默认没有密码! 限制客户端连接相关 maxclients 10000 #能连接上redis-server的最大客户端数量 maxmemory bytes #redis占用的最大的内存容量 maxmemory-policy noeviction # 内存达到限制值时的处理策略redis 中的默认的过期策略是 volatile-lru 。 设置方式 config set maxmemory-policy volatile-lru 1maxmemory-policy 六种方式 1、volatile-lru只对设置了过期时间的key进行LRU默认值 2、allkeys-lru 删除lru算法的key 3、volatile-random随机删除即将过期key 4、allkeys-random随机删除 5、volatile-ttl 删除即将过期的 6、noeviction永不过期返回错误 AOF相关部分 默认是不开启aof模式的,默认是使用RDB方式持久化的,因为在大部分时间里,rdb完全够用!! 十、持久化—RDB 工作面试中持久化是重点! Redis是内存数据库,如果不将内存中的数据保存到磁盘,那么一旦服务器进程退出,服务器中的数据库也会消失。 所以Redis提供了持久化功能!! 什么是RDB 在指定时间间隔后将内存中的数据集快照写入数据库 在恢复时候直接读取快照文件进行数据的恢复 默认情况下 Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件中进行自定义。 工作原理 在进行 RDB 的时候redis 的主线程是不会做 io 操作的主线程会 fork 一个子线程来完成该操作 Redis 调用forks。同时拥有父进程和子进程。子进程将数据集写入到一个临时 RDB 文件中。当子进程完成对新 RDB 文件的写入时Redis 用新 RDB 文件替换原来的 RDB 文件并删除旧的 RDB 文件。 这种工作方式使得 Redis 可以从写时复制copy-on-write机制中获益(因为是使用子进程进行写操作而父进程依然可以接收来自客户端的请求。) 触发机制 save的规则满足的情况下会自动触发rdb原则执行flushall命令也会触发我们的rdb原则退出redis也会自动产生rdb文件 save 使用 save 命令会立刻对当前内存中的数据进行持久化 ,但是会阻塞也就是不接受其他操作了 由于 save 命令是同步命令会占用Redis的主进程。若Redis数据非常多时save命令执行速度会非常慢阻塞所有客户端的请求。 flushall命令 flushall 命令也会触发持久化 触发持久化规则 满足配置条件中的触发条件 ,也会触发持久化; 可以通过配置文件对 Redis 进行设置 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时 自动进行数据集保存操作。 bgsave bgsave 是异步进行进行持久化的时候redis 还可以将继续响应客户端请求 bgsave和save对比 命令savebgsaveIO类型同步异步阻塞是是阻塞发生在fork()通常非常快复杂度O(n)O(n)优点不会消耗额外的内存不阻塞客户端命令缺点阻塞客户端命令需要fork子进程消耗内存 RDB优缺点 优点 适合大规模的数据恢复对数据的完整性要求不高 缺点 需要一定的时间间隔进行操作如果redis意外宕机了这个最后一次修改的数据就没有了。即最后一次持久化的数据可能丢失fork进程的时候会占用一定的内容空间。 十一、持久化AOF Append Only File 将我们所有的写命令都记录下来,恢复的时候就把这个文件中的命令全部再执行一遍 以日志的形式来记录每个写的操作将Redis执行过的所有指令记录下来读操作不记录只许追加文件但不可以改写文件redis启动之初会读取该文件重新构建数据换言之redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 什么是AOF 快照功能RDB并不是非常耐久durable 如果 Redis 因为某些原因而造成故障停机 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始 Redis 增加了一种完全耐久的持久化方式 AOF 持久化。 如果要使用AOF需要修改配置文件 appendonly yes则表示启用AOF 默认是不开启的我们需要手动配置然后重启redis就可以生效了 如果这个aof文件有错位这时候redis是启动不起来的我需要修复这个aof文件,redis给我们提供了一个工具redis-check-aof --fix 如果文件正常,重启就可以直接恢复了。 优点和缺点 appendonly yes # 默认是不开启aof模式的默认是使用rdb方式持久化的在大部分的情况下rdb完全够用 appendfilename appendonly.aof #默认文件名# appendfsync always # 每次修改都会sync 消耗性能 appendfsync everysec # 每秒执行一次 sync 可能会丢失这一秒的数据 # appendfsync no # 不执行 sync ,这时候操作系统自己同步数据速度最快优点 每一次修改都会同步文件的完整性会更加好每秒同步一次可能会丢失一秒的数据从不同步效率最高 缺点 相对于数据文件来说aof远远大于rdb修复速度比rdb慢Aof运行效率也要比rdb慢所以我们redis默认的配置就是rdb持久化aof默认就是文件的无限追加,文件会越来越大! 十二、RDB和AOP选择 RDB 和 AOF 对比 RDBAOF启动优先级低高体积小大恢复速度快慢数据安全性丢数据根据策略决定 如何选择使用哪种持久化方式 一般来说 如果想达到足以媲美 PostgreSQL 的数据安全性 你应该同时使用两种持久化功能。 如果你希望数据只在服务器运行的时候存在,也可不进行持久化,只用作缓存使用。 如果你非常关心你的数据 但仍然可以承受数分钟以内的数据丢失 那么你可以只使用 RDB 持久化。 有很多用户都只使用 AOF 持久化 但并不推荐这种方式 因为定时生成 RDB 快照snapshot非常便于进行数据库备份 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。 十三、Redis发布与订阅 Redis 发布订阅(pub/sub)是一种消息通信模式发送者(pub)发送消息订阅者(sub)接收消息。 Redis客户端可以订阅任意数量的频道! 下图展示了频道 channel1 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系 当有新消息通过 PUBLISH 命令发送给频道 channel1 时 这个消息就会被发送给订阅它的三个客户端 命令 这些命令被广泛用于构建即时通信应用,比如实时广播和实时提醒。 命令描述PSUBSCRIBE pattern [pattern..]订阅一个或多个符合给定模式的频道。PUNSUBSCRIBE pattern [pattern..]退订一个或多个符合给定模式的频道。PUBSUB subcommand [argument[argument]]查看订阅与发布系统状态。PUBLISH channel message向指定频道发布消息SUBSCRIBE channel [channel..]订阅给定的一个或多个频道。UNSUBSCRIBE channel [channel..]退订一个或多个频道 示例 ------------订阅端---------------------- 127.0.0.1:6379 SUBSCRIBE sakura # 订阅sakura频道 Reading messages... (press Ctrl-C to quit) # 显示订阅的信息 1) subscribe # 订阅成功的消息 2) sakura 3) (integer) 1 # 等待读取推送的消息 1) message # 表明是消息 2) sakura #频道名 3) hello world#消息内容 # 等待读取推送的消息 # 接收到来自sakura频道的消息 hello i am sakura 1) message# 表明是消息 2) sakura#频道名 3) hello i am sakura#消息内容--------------消息发布端------------------- 127.0.0.1:6379 PUBLISH sakura hello world # 发布消息到sakura频道 (integer) 1 127.0.0.1:6379 PUBLISH sakura hello i am sakura # 发布消息 (integer) 1-----------------查看活跃的频道------------ 127.0.0.1:6379 PUBSUB channels 1) sakura原理 每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构 结构的 pubsub_channels 属性是一个字典 这个字典就用于保存订阅频道的信息其中字典的键为正在被订阅的频道 而字典的值则是一个链表 链表中保存了所有订阅这个频道的客户端。 客户端订阅就被链接到对应频道的链表的尾部退订则就是将客户端节点从链表中移除。 缺点 如果一个客户端订阅了频道但自己读取消息的速度却不够快的话那么不断积压的消息会使redis输出缓冲区的体积变得越来越大这可能使得redis本身的速度变慢甚至直接崩溃。这和数据传输可靠性有关如果在订阅方断线那么他将会丢失所有在短线期间发布者发布的消息。 应用 消息订阅公众号订阅微博关注等等起始更多是使用消息队列来进行实现多人在线聊天室。 稍微复杂的场景我们就会使用消息中间件MQ处理。 十四、Redis主从复制 概念 主从复制是指将一台Redis服务器的数据复制到其他的Redis服务器。前者称为主节点Master/Leader,后者称为从节点Slave/Follower 数据的复制是单向的只能由主节点复制到从节点主节点以写为主、从节点以读为主。 默认情况下每台Redis服务器都是主节点一个主节点可以有0个或者多个从节点但每个从节点只能有一个主节点。 作用 数据冗余主从复制实现了数据的热备份是持久化之外的一种数据冗余的方式。故障恢复当主节点故障时从节点可以暂时替代主节点提供服务是一种服务冗余的方式负载均衡在主从复制的基础上配合读写分离由主节点进行写操作从节点进行读操作分担服务器的负载尤其是在多读少写的场景下通过多个从节点分担负载提高并发量。高可用基石主从复制是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。 为什么使用集群 单台服务器难以负载大量的请求单台服务器故障率高系统崩坏概率大单台服务器内存容量有限。 环境配置 在讲解redis配置文件的时候注意到有一个replication模块 查看当前库的信息info replication 127.0.0.1:6379 info replication # Replication role:master # 角色 connected_slaves:0 # 从机数量 master_replid:3b54deef5b7b7b7f7dd8acefa23be48879b4fcff 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既然需要启动多个redis服务就需要复制多个redis的配置文件。 其中的每个配置文件对应修改以下信息 端口号pid文件名日志文件名rdb文件名 修改完毕后,以复制3个配置文件为例,启动3个redis服务器, 可以通过进程信息查看!如下:[启动单机多服务集群] 一主二从配置 默认情况下每台Redis服务器都是主节点 我们一般情况下只用配置从机就好了 认老大一主6379二从63806381 只需要先连接相应端口的redis server,再在其中使用SLAVEOF host port命令就可以为从机配置主机了,如下: 然后主机上也能看到从机的状态 我们这里是使用命令搭建是暂时的。 真实开发中是在从机配置文件中进行配置这样的话是永久的。 见如下的配置文件: 使用规则 从机只能读不能写主机可读可写但是多用于写。 127.0.0.1:6381 set name sakura # 从机6381写入失败 (error) READONLY You cant write against a read only replica.127.0.0.1:6380 set name sakura # 从机6380写入失败 (error) READONLY You cant write against a read only replica.127.0.0.1:6379 set name sakura OK 127.0.0.1:6379 get name sakura当主机断电宕机后默认情况下从机的角色不会发生变化 集群中只是失去了写操作当主机恢复以后又会连接上从机恢复原状。 当从机断电宕机后若不是使用配置文件配置的从机再次启动后作为主机是无法获取之前主机的数据的若此时重新配置称为从机又可以获取到主机的所有数据。这里就要提到一个同步原理。 第二条中提到默认情况下主机故障后不会出现新的主机有两种方式可以产生新的主机 从机手动执行命令slaveof no one,这样执行以后从机会独立出来成为一个主机 (手动选举)使用哨兵模式自动选举 如果没有老大了这个时候能不能选择出来一个老大呢手动 如果主机断开了连接我们可以使用SLAVEOF no one让自己变成主机其他的节点就可以手动连接到最新的主节点手动如果这个时候老大修复了那么就重新连接 十五、哨兵模式 更多信息参考博客https://www.jianshu.com/p/06ab9daf921d 主从切换技术的方法是当主服务器宕机后需要手动把一台从服务器切换为主服务器这就需要人工干预费事费力还会造成一段时间内服务不可用。这不是一种推荐的方式更多时候我们优先考虑哨兵模式。 单机单个哨兵 哨兵的作用 通过发送命令让Redis服务器返回监控其运行状态包括主服务器和从服务器。当哨兵监测到master宕机会自动将slave切换成master然后通过发布订阅模式通知其他的从服务器修改配置文件让它们切换主机。 多哨兵模式 创建一个哨兵的核心配置文件:sentinel.conf #sentinel monitor 被监控的名称 host port 1 sentinel monitor mymaster 127.0.0.1 6379 1数字1表示 当一个哨兵主观认为主机断开就可以客观认为主机故障然后开始选举新的主机。 测试 启动哨兵: redis-sentinel xxx/sentinel.conf成功启动哨兵模式 此时哨兵监视着我们的主机6379当我们断开主机后 哨兵模式优缺点 优点 哨兵集群基于主从复制模式所有主从复制的优点它都有主从可以切换故障可以转移系统的可用性更好哨兵模式是主从模式的升级手动到自动更加健壮 缺点 Redis不好在线扩容集群容量一旦达到上限在线扩容就十分麻烦实现哨兵模式的配置其实是很麻烦的里面有很多配置项 哨兵模式的全部配置详解: 完整的哨兵模式配置文件中可以配置的东西 sentinel.conf # Example sentinel.conf# 哨兵sentinel实例运行的端口 默认26379 port 26379# 哨兵sentinel的工作目录 dir /tmp# 哨兵sentinel监控的redis主节点的 ip port # master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符.-_组成。 # quorum 当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了 # sentinel monitor master-name ip redis-port quorum sentinel monitor mymaster 127.0.0.1 6379 1# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码 # 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码 # sentinel auth-pass master-name password sentinel auth-pass mymaster MySUPER--secret-0123passw0rd# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒 # sentinel down-after-milliseconds master-name milliseconds sentinel down-after-milliseconds mymaster 30000# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步 这个数字越小完成failover所需的时间就越长 但是如果这个数字越大就意味着越 多的slave因为replication而不可用。 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。 # sentinel parallel-syncs master-name numslaves sentinel parallel-syncs mymaster 1# 故障转移的超时时间 failover-timeout 可以用在以下这些方面 #1. 同一个sentinel对同一个master两次failover之间的间隔时间。 #2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 #3.当想要取消一个正在进行的failover所需要的时间。 #4.当进行failover时配置所有slaves指向新的master所需的最大时间。不过即使过了这个超时slaves依然会被正确配置为指向master但是就不按parallel-syncs所配置的规则来了 # 默认三分钟 # sentinel failover-timeout master-name milliseconds sentinel failover-timeout mymaster 180000# SCRIPTS EXECUTION#配置当某一事件发生时所需要执行的脚本可以通过脚本来通知管理员例如当系统运行不正常时发邮件通知相关人员。 #对于脚本的运行结果有以下规则 #若脚本执行后返回1那么该脚本稍后将会被再次执行重复次数目前默认为10 #若脚本执行后返回2或者比2更高的一个返回值脚本将不会重复执行。 #如果脚本在执行过程中由于收到系统中断信号被终止了则同返回值为1时的行为相同。 #一个脚本的最大执行时间为60s如果超过这个时间脚本将会被一个SIGKILL信号终止之后重新执行。#通知型脚本:当sentinel有任何警告级别的事件发生时比如说redis实例的主观失效和客观失效等等将会去调用这个脚本 #这时这个脚本应该通过邮件SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时将传给脚本两个参数 #一个是事件的类型 #一个是事件的描述。 #如果sentinel.conf配置文件中配置了这个脚本路径那么必须保证这个脚本存在于这个路径并且是可执行的否则sentinel无法正常启动成功。 #通知脚本 # sentinel notification-script master-name script-pathsentinel notification-script mymaster /var/redis/notify.sh# 客户端重新配置主节点参数脚本 # 当一个master由于failover而发生改变时这个脚本将会被调用通知相关的客户端关于master地址已经发生改变的信息。 # 以下参数将会在调用脚本时传给脚本: # master-name role state from-ip from-port to-ip to-port # 目前state总是“failover”, # role是“leader”或者“observer”中的一个。 # 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的 # 这个脚本应该是通用的能被多次调用不是针对性的。 # sentinel client-reconfig-script master-name script-path sentinel client-reconfig-script mymaster /var/redis/reconfig.sh十六、缓存穿透与雪崩 缓存穿透查不到 概念 在默认情况下用户请求数据时会先在缓存(Redis)中查找若没找到即缓存未命中再在数据库中进行查找数量少可能问题不大可是一旦大量的请求数据例如秒杀场景缓存都没有命中的话就会全部转移到数据库上造成数据库极大的压力就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击被称为洪水攻击。 解决方案 布隆过滤器 对所有可能查询的参数以Hash的形式存储以便快速确定是否存在这个值在控制层先进行拦截校验校验不通过直接打回减轻了存储系统的压力。 缓存空对象 一次请求若在缓存和数据库中都没找到就在缓存中方一个空对象用于处理后续这个请求。 这样做有一个缺陷存储空对象也需要空间大量的空对象会耗费一定的空间存储效率并不高。解决这个缺陷的方式就是设置较短过期时间 即使对空值设置了过期时间还是会存在缓存层和存储层的数据会有一段时间窗口的不一致这对于需要保持一致性的业务会有影响。 缓存击穿量太大缓存过期 概念 相较于缓存穿透缓存击穿的目的性更强一个存在的key在缓存过期的一刻同时有大量的请求这些请求都会击穿到DB造成瞬时DB请求量大、压力骤增。这就是缓存被击穿只是针对其中某个key的缓存不可用而导致击穿但是其他的key依然可以使用缓存响应。 比如热搜排行上一个热点新闻被同时大量访问就可能导致缓存击穿。 解决方案 设置热点数据永不过期 这样就不会出现热点数据过期的情况但是当Redis内存空间满的时候也会清理部分数据而且此种方案会占用空间一旦热点数据多了起来就会占用部分空间。 加互斥锁(分布式锁) 在访问key之前采用SETNXset if not exists来设置另一个短期key来锁住当前key的访问访问结束再删除该短期key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。 缓存雪崩 概念 大量的key设置了相同的过期时间导致在缓存在同一时刻全部失效造成瞬时DB请求量大、压力骤增引起雪崩。 解决方案 redis高可用 这个思想的含义是既然redis有可能挂掉那我多增设几台redis这样一台挂掉之后其他的还可以继续工作其实就是搭建的集群 限流降级 这个解决方案的思想是在缓存失效后通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存其他线程等待。 数据预热 数据加热的含义就是在正式部署之前我先把可能的数据先预先访问一遍这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key设置不同的过期时间让缓存失效的时间点尽量均匀。 创作不易,欢迎点赞/收藏!!
http://www.sczhlp.com/news/163133/

相关文章:

  • 个人网站做哪些内容成都定制网站建设
  • 怎么把网站链接做二维码国内最新十大新闻
  • 两学一做网站注册led行业网站建设方案
  • 网站推广方案虚拟空间怎么做网站目录指向
  • 有没有类似一起做网店的网站wordpress插件 知乎
  • 成都网站搭建公司哪家好做商品网站需要营业执照
  • 如何做图片网站如何建设网站咨询跳转页面
  • 大理州住房和城乡建设局网站nginx 运行wordpress
  • 上海崇明林业建设有限公司网站网站设计中的js是什么
  • 学习方法
  • ai提交消息常用的 chore,原来是个单词(琐事/零散任务)+约定,用于非功能性提交
  • 网站开发项目教程创建网页快捷键
  • 留学网站建设大数据营销平台有哪些
  • 咋样建设网站网站建设需要固定ip地址吗
  • 食品公司网站建设石家庄建站软件
  • 制作网站专业公司吗视频wordpress源码
  • 成武县建设局网站全国工商信息企业查询官网
  • 织梦模板大气网站建设类网站模板微信小程序平台登录入口
  • 海淀网站开发wordpress页面怎么添加
  • 个人网站建站系统专业的免费建站
  • 做网站买什么空间百度推广账户登录
  • 互联网品牌是什么意思郴州seo快速排名
  • 个人做网站好吗wordpress 淘客网站
  • 泉山网站开发解释seo网站推广
  • 网站代码框架北京社交网站建设
  • 广西南宁网站建设有限公司怎么做电商赚钱
  • 网站销售怎么样的易观数据
  • 汕头制作公司网站禅城区网站建站建设
  • 网站字体样式天津百度
  • 如何衡量网站的价值网站空间申请开通