NoSQL数据库简介
技术发展
技术的分类
- 解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
- 解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis
- 解决性能的问题:NoSQL、Java线程、Hadoop、Nginx、MQ、ElasticSearch
NoSQL数据库
NoSQL数据库概述
-
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。 NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
- 不遵循SQL标准。
- 不支持ACID。
- 远超于SQL的性能。
-
NoSQL适用场景
- 对数据高并发的读写
- 海量数据的读写
- 对数据高可扩展性的
其他NoSql
- Memcache
- 很早出现的NoSql数据库
- 数据都在内存中,一般不持久化
- 支持简单的key-value模式,支持类型单一
- 一般是作为缓存数据库辅助持久化的数据库
- Redis
- 几乎覆盖了Memcached的绝大部分功能
- 数据都在内存中,支持持久化,主要用作备份恢复
- 除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
- 一般是作为缓存数据库辅助持久化的数据库
- MongoDB
- 高性能、开源、模式自由(schema free)的文档型数据库
- 数据都在内存中, 如果内存不足,把不常用的数据保存到硬盘
- 虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
- 支持二进制数据及大型对象
- 可以根据数据的特点替代RDBMS ,成为独立的数据库。或者配合RDBMS,存储特定的数据
Redis概述安装
概述
- Redis是一个开源的key-value存储系统。
- 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。
- 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
- 在此基础上,Redis支持各种不同方式的排序。
- 与memcached一样,为了保证效率,数据都是缓存在内存中。
- 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。
- 并且在此基础上实现了master-slave(主从)同步。
Redis安装
编译安装-略
准备工作:下载安装最新版的gcc编译器
- 安装C 语言的编译环境
yum install centos-release-scl scl-utils-build
yum install -y devtoolset-8-toolchain
scl enable devtoolset-8 bash
- 测试 gcc版本
gcc --version
安装
- 下载redis-6.2.1.tar.gz放/usr/local/bin目录
- tar -zxvf redis-6.2.1.tar.gz
- cd redis-6.2.1
- 在redis-6.2.1目录下再次执行make命令(只是编译好)
- make install
启动
前台启动(不推荐)
- 前台启动,命令行窗口不能关闭,否则服务器停止
src/redis-server
后台启动
- 修改配置
// 让服务在后台启动
daemonize no改成yes
- 拷贝一份redis.conf到其他目录
cp /redis.conf /myredis/redis_6379.conf
redis-server /myredis/redis.conf
docker安装
docker pull redis
// 有点小bug,需要先创建mydata/redis/conf目录
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
# 自动开启
sudo docker update redis --restart=always
# 客户端链接
docker exec -it redis redis-cli
遇到的问题
make 会报错
- 如果没有准备好C语言编译环境,make 会报错—Jemalloc/jemalloc.h:没有那个文件
- 解决方案:运行make distclean
redis无法启动
- docker挂载的目录需要权限
redis无法外部访问
- 关闭selinux
vi /etc/selinux/config
SELINUX=enforcing改为SELINUX=disabled
// 重启生效
- 关闭防火墙
// 添加
firewall-cmd --zone=public --add-port=6379/tcp --permanent (--permanent永久生效,没有此参数重启后失效)
// 重新载入
firewall-cmd --reload
// 查看
firewall-cmd --zone= public --query-port=6379/tcp
- 配置文件
// 白名单
bind 0.0.0.0
// 关闭保护
protected-mode no
// 连接允许-没测试
client-output-buffer-limit slave 1024mb 256mb 0
// 是否选择关闭密码
// 如果不关闭密码,访问都携带密码
# requirepass
Redis介绍相关知识
-
端口6379从何而来Alessia Merz
-
数据库
- 默认16个数据库,类似数组下标从0开始,初始默认使用0号库
- 使用命令 select <dbid>来切换数据库。如: select 8 统一密码管理,所有库同样密码。
- dbsize查看当前数据库的key的数量
- flushdb清空当前库
- flushall通杀全部库
-
Redis是单线程+多路IO复用技术
- 多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)
常用五大数据类型
Redis键(key)和数据库
-
keys *查看当前库所有key (匹配:keys *1)
-
exists key判断某个key是否存在
-
type key 查看你的key是什么类型
-
del key 删除指定的key数据
-
unlink key 根据value选择非阻塞删除
-
仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。
-
expire key 10 10秒钟:为给定的key设置过期时间
-
ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期
-
select命令切换数据库
-
dbsize查看当前数据库的key的数量
-
flushdb清空当前库
-
flushall通杀全部库
字符串(String)
概念
- String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
- String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
- String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
数据结构
- String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
- 如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
命令
-
set <key><value>添加键值对
- *NX:当数据库中key不存在时,可以将key-value添加数据库
- *XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
- *EX:key的超时秒数
- *PX:key的超时毫秒数,与EX互斥
-
get 查询对应键值
-
append 将给定的 追加到原值的末尾
-
strlen 获得值的长度
-
incr <key>
将 key 中储存的数字值增1
只能对数字值操作,如果为空,新增值为1 -
decr <key>
将 key 中储存的数字值减1
只能对数字值操作,如果为空,新增值为-1
incrby / decrby <key><步长>将 key 中储存的数字值增减。自定义步长。 -
mset <key1><value1><key2><value2> … 同时设置一个或多个 key-value对
-
mget <key1><key2><key3> … 同时获取一个或多个 value
-
msetnx <key1><value1><key2><value2> … 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。原子性,有一个失败则都失败
-
getrange <key><起始位置><结束位置> 获得值的范围,类似java中的substring,前包,后包
-
setrange <key><起始位置><value> 用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。
-
setex <key><过期时间><value> 设置键值的同时,设置过期时间,单位秒。
-
getset <key><value> 以新换旧,设置了新值同时获得旧值。
###################################################string#####################
127.0.0.1:6379> set zgc hello
OK
127.0.0.1:6379> append zgc word
(integer) 9
127.0.0.1:6379> get zgc
"helloword"
127.0.0.1:6379> strlen zgc # 字符串长度
(integer) 9
127.0.0.1:6379> set count 0
OK
127.0.0.1:6379> type count # 类型,此刻的数字也是类型
string
127.0.0.1:6379> incr count # 自增1
(integer) 1
127.0.0.1:6379> incr count
(integer) 2
127.0.0.1:6379> get count
"2"
127.0.0.1:6379> incrby zgc 10 # 非数字会报错
(error) ERR value is not an integer or out of range
127.0.0.1:6379> incrby count 10 # 设置步长10
(integer) 12
127.0.0.1:6379> get count
"12"
127.0.0.1:6379> GETRANGE zgc 0 -1 # 获取全部(get key一样)
"helloword"
127.0.0.1:6379> getrange zgc 1 2 # 截至指定下标,[1,2]
"el"
127.0.0.1:6379> setrange zgc -4 ,im # 不能负数下标
(error) ERR offset is out of range
127.0.0.1:6379> setrange zgc 5 ,im # 替换,下标开始替换,被替换的长度是替换长度决定
(integer) 9
127.0.0.1:6379> get zgc
"hello,imd"
127.0.0.1:6379> setex guan 10 hhhhh # 设置key并且有过期时间
OK
127.0.0.1:6379> ttl guan # 查看过期时间
(integer) 7
127.0.0.1:6379> setnx my 123 # 不存在则设置,成功 1 失败 0
(integer) 1
127.0.0.1:6379> setnx my 3333
(integer) 0
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 批量设置key-value
OK
127.0.0.1:6379> mget k1 k2 k3 # 批量获取
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> mset k1 v11 k4 v4
OK
127.0.0.1:6379> mget k1 k4
1) "v11"
2) "v4"
127.0.0.1:6379> msetnx k4 v44 k5 v5 # 批量不存在则设置,具有原子性(一起成功或者一起失败)
(integer) 0
127.0.0.1:6379> mget k4 k5
1) "v4"
2) (nil)
127.0.0.1:6379> getset you 123 # 先获取再设置
(nil)
127.0.0.1:6379> getset you 22
"123"
127.0.0.1:6379> get you
"22"
小结:
计数器
统计多单位的数量
粉丝数
对象缓存存储!
list列表
概念
单键多值,Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
数据结构
- List的数据结构为快速链表quickList
- 首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存
- 当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
- Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
命令
- lpush/rpush <key><value1><value2><value3> … 从左边/右边插入一个或多个值。
- lpop/rpop <key>从左边/右边吐出一个值。值在键在,值光键亡。
- rpoplpush <key1><key2>从<key1>列表右边吐出一个值,插到<key2>列表左边。
- lrange <key><start><stop> 按照索引下标获得元素(从左到右)
- lrange mylist 0 -1 0左边第一个,-1右边第一个,(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
##########################################list########################################
127.0.0.1:6379> lpush fan 1 2 3 4 5 # 左边进入
(integer) 5
127.0.0.1:6379> lrange fan 0 2 # 显示[0,2]
1) "5"
2) "4"
3) "3"
127.0.0.1:6379> lrange fan 0 -1 # 显示全部
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> rpush fan 88 99 # 右边进入
(integer) 7
127.0.0.1:6379> lrange fan 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
6) "88"
7) "99"
127.0.0.1:6379> lpop fan # 最左边一个弹出
"5"
127.0.0.1:6379> rpop fan #最右边一个弹出
"99"
127.0.0.1:6379> lrange fan 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "88"
127.0.0.1:6379> llen fan # 列表长度
(integer) 5
127.0.0.1:6379> lindex fan 4 # 显示指定下标的值
"88"
127.0.0.1:6379> lrange fan 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
5) "88"
127.0.0.1:6379> lrem fan 1 4 # 删除 list count value,失败的话返回0
(integer) 1
127.0.0.1:6379> lrange fan 0 -1
1) "3"
2) "2"
3) "1"
4) "88"
127.0.0.1:6379> ltrim fan 1 2 # 通过下标截取,原list被改变
OK
127.0.0.1:6379> lrange fan 0 -1
1) "2"
2) "1"
127.0.0.1:6379> rpoplpush fan fan2 # 移除列表的最后一个元素(原list被改变),将他移动到新的列表中!
"1"
127.0.0.1:6379> lrange fan 0 -1
1) "2"
127.0.0.1:6379> lrange fan2 0 -1
1) "1"
127.0.0.1:6379> lrange fan 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> lset fan 2 two2 # 通过下标更新某值
OK
127.0.0.1:6379> lrange fan 0 -1
1) "four"
2) "three"
3) "two2"
4) "one"
127.0.0.1:6379> lset fan 8 88 当lset指定的index不存在时,报错
(error) ERR index out of range
127.0.0.1:6379> linsert fan before two2 aaaaaaa # 指定value,以before/after插入list
(integer) 5
127.0.0.1:6379> lrange fan 0 -1
1) "four"
2) "three"
3) "aaaaaaa"
4) "two2"
5) "one"
小结:
他实际上是一个链表,before Node after , left,right 都可以插入值
如果key 不存在,创建新的链表 如果key存在,新增内容 如果移除了所有值,空链表,也代表不存在!
在两边插入或者改动值,效率最高! 中间元素,相对来说效率会低一点~
消息排队!消息队列 (Lpush Rpop), 栈( Lpush Lpop)!
集合(Set)
概念
- Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
- Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
- 一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变
数据结构
- Set数据结构是dict字典,字典是用哈希表实现的。
- Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
命令
- sadd <key><value1><value2> … 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
- smembers <key>取出该集合的所有值。
- sismember <key><value>判断集合<key>是否为含有该<value>值,有1,没有0
- scard<key>返回该集合的元素个数。
- srem <key><value1><value2> … 删除集合中的某个元素。
- spop <key>随机从该集合中吐出一个值。
- srandmember <key><n>随机从该集合中取出n个值。不会从集合中删除 。
- smove <source><destination>value把集合中一个值从一个集合移动到另一个集合
- sinter <key1><key2>返回两个集合的交集元素。
- sunion <key1><key2>返回两个集合的并集元素。
- sdiff <key1><key2>返回两个集合的差集元素(key1中的,不包含key2中的)
##########################################set########################################
127.0.0.1:6379> sadd myset aa bb cc dd # 添加值
(integer) 4
127.0.0.1:6379> smembers myset # 查看所有
1) "dd"
2) "aa"
3) "cc"
4) "bb"
127.0.0.1:6379> sismember myset cc # 判断是否存在某个值
(integer) 1
127.0.0.1:6379> sismember myset ff
(integer) 0
127.0.0.1:6379> scard myset # 集合个数
(integer) 4
127.0.0.1:6379> srem myset dd ff # 删除,指定value
(integer) 2
127.0.0.1:6379> smembers myset
1) "bb"
2) "cc"
3) "aa"
127.0.0.1:6379> srandmember myset 1 # 随机获取N个value
1) "cc"
127.0.0.1:6379> srandmember myset 1
1) "aa"
127.0.0.1:6379> spop myset 1 # 随机删除N个
"bb"
127.0.0.1:6379> spop myset
"cc"
127.0.0.1:6379> smembers myset
1) "aa"
127.0.0.1:6379> smove myset myset2 v2 # 一个集合剪切value给另一个集合
(integer) 1
127.0.0.1:6379> smembers myset
1) "v3"
2) "v1"
127.0.0.1:6379> smembers myset2
1) "v1"
2) "v2"
127.0.0.1:6379> sdiff myset myset2 # 差集(以第一个为中心)
1) "v3"
127.0.0.1:6379> sinter myset myset2 #交集
1) "v1"
127.0.0.1:6379> sunion myset myset2 #并集
1) "v1"
2) "v3"
3) "v2"
小结:
无序不重复集合
微博,A用户将所有关注的人放在一个set集合中!将它的粉丝也放在一个集合中!共同关注,共同爱好,二度好友,推荐好友!(六度分割理论)
哈希(Hash)
概念
- Redis hash 是一个键值对集合。
- Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map<String,Object>
- 主要有以下2种存储方式:
数据结构
- Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
命令
- hset <key><field><value>给<key>集合中的 <field>键赋值<value>
- hget <key1><field>从<key1>集合<field>取出 value
- hmset <key1><field1><value1><field2><value2>… 批量设置hash的值
- hexists<key1><field>查看哈希表 key 中,给定域 field 是否存在。
- hkeys <key>列出该hash集合的所有field
- hvals <key>列出该hash集合的所有value
- hincrby <key><field><increment>为哈希表 key 中的域 field 的值加上增量 1 -1
- hsetnx <key><field><value>将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
##########################################hash########################################
127.0.0.1:6379> hset myhash v1 k1 # 设置hash单个key-value
(integer) 1
127.0.0.1:6379> hget myhash v1 # 获取hash单个vlaue
"k1"
127.0.0.1:6379> hmset myhash v2 k2 v3 k3 # 设置多个key-value
OK
127.0.0.1:6379> hmget myhash v1 v2 v3 # 获取多个value
1) "k1"
2) "k2"
3) "k3"
127.0.0.1:6379> hgetall myhash # 获取所有的key-value
1) "v1"
2) "k1"
3) "v2"
4) "k2"
5) "v3"
6) "k3"
127.0.0.1:6379> hdel myhash v1 # 删除指定的key
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "v2"
2) "k2"
3) "v3"
4) "k3"
127.0.0.1:6379> hlen myhash # hash长度
(integer) 2
127.0.0.1:6379> hexists myhash v1 # 判断某个key是否存在
(integer) 0
127.0.0.1:6379> hexists myhash v2
(integer) 1
127.0.0.1:6379> hkeys myhash # hash所有的key
1) "v2"
2) "v3"
127.0.0.1:6379> hvals myhash # hash所有的value
1) "k2"
2) "k3"
127.0.0.1:6379> hset myhash v4 5
(integer) 1
127.0.0.1:6379> hincrby myhash v4 1 # 指定key自增
(integer) 6
127.0.0.1:6379> hsetnx myhash v5 k5 # hash不存在key则新建,成功返回1 失败返回0
(integer) 1
127.0.0.1:6379> hsetnx myhash v5 k55
(integer) 0
小结:
Map集合,key-map! 时候这个值是一个map集合! 本质和String类型没有太大区别,还是一个简单的 key-vlaue!
有序集合Zset(sorted set)
概念
- Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
- 不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
- 因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
- 访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
数据结构
- SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
- zset底层使用了两个数据结构
- hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
- 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
跳跃表(跳表)
简介
- 有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。
- Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
实例
- 对比有序链表和跳跃表,从链表中查询出51
命令
- zadd <key><score1><value1><score2><value2>… 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
- zrange <key><start><stop> [WITHSCORES] 返回有序集 key 中,下标在<start><stop>之间的元素, 带WITHSCORES,可以让分数一起和值返回到结果集。
- zrangebyscore key minmax [withscores] [limit offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
- zrevrangebyscore key maxmin [withscores] [limit offset count] 同上,改为从大到小排列。
- zincrby <key><increment><value> 为元素的score加上增量
- zrem <key><value>删除该集合下,指定值的元素
- zcount <key><min><max>统计该集合,分数区间内的元素个数
- zrank <key><value>返回该值在集合中的排名,从0开始。
##########################################Zset########################################
127.0.0.1:6379> zadd salary 2500 aa 5000 bb 800 cc # 添加
(integer) 3
127.0.0.1:6379> zrange salary 0 -1 # 显示所有
1) "cc"
2) "aa"
3) "bb"
127.0.0.1:6379> zrangebyscore salary -inf +inf # zrangebyscore ,升序显示, -inf +inf 范围,负无穷,正无穷
1) "cc"
2) "aa"
3) "bb"
127.0.0.1:6379>
127.0.0.1:6379> zrevrange salary 0 -1 # zrevrange ,降序显示
1) "bb"
2) "aa"
3) "cc"
127.0.0.1:6379> zrevrange salary 0 -1 withscores # 降序排序,带key显示
1) "bb"
2) "5000"
3) "aa"
4) "2500"
5) "cc"
6) "800"
127.0.0.1:6379> zrangebyscore salary -inf 3000 withscores # 升序排序,范围,(负无穷,3000],带key,
1) "cc"
2) "800"
3) "aa"
4) "2500"
127.0.0.1:6379> zcard salary # 查询个数
(integer) 3
127.0.0.1:6379> zrem salary cc # 删除,指定value
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "aa"
2) "bb"
127.0.0.1:6379> zcount salary 5000 6000 # 统计key在[5000,6000]区间的个数
小结:
有序集合
普通消息,1, 重要消息 2,带权重进行判断!
Redis配置文件介绍
Units单位
- 配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit,大小写不敏感
INCLUDES包含
- 可以把公用的配置文件提取出来
网络相关配置
bind
- 默认情况bind=127.0.0.1只能接受本机的访问请求,不写的情况下,无限制接受任何ip地址的访问
- 生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉
- 如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应
protected-mode
- 访问保护模式,一般设置no
Port
- 端口号,默认 6379
tcp-backlog
- 设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列
- 设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列
- 注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128),所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果
timeout
- 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。
tcp-keepalive
- 对访问客户端的一种心跳检测,每个n秒检测一次。
- 单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
GENERAL通用
daemonize
- 是否为后台进程,设置为yes
- 守护进程,后台启动
pidfile
- 存放pid文件的位置,每个实例会产生一个不同的pid文件
loglevel
- 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice
- 四个级别根据使用阶段来选择,生产环境选择notice 或者warning
logfile
- 日志文件名称
databases 16
- 设定库的数量 默认16,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
SECURITY安全
临时设置密码
- 在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。
永久设置密码
- 永久设置密码,需要再配置文件中进行设置。
LIMITS限制
maxclients
- 设置redis同时可以与多少个客户端进行连接。
- 默认情况下为10000个客户端。
- 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。
maxmemory
- 建议必须设置,否则,将内存占满,造成服务器宕机
- 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
- 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
- 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。
maxmemory-policy
- reids一旦到达内存使用上限,移除规则
- volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
- allkeys-lru:在所有集合key中,使用LRU算法移除key
- volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
- allkeys-random:在所有集合key中,移除随机的key
- volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
- noeviction:不进行移除。针对写操作,只是返回错误信息
maxmemory-samples
- 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。
- 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。
Redis的发布和订阅
什么是发布和订阅
-
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
-
Redis 客户端可以订阅任意数量的频道。
Redis的发布和订阅
- 客户端可以订阅频道如下图
- 当给这个频道发布消息后,消息就会发送给订阅的客户端
发布订阅命令行实现
-
客户端订阅频道,SUBSCRIBE 频道名
-
频道发布消息,publish 频道名 消息
-
订阅客户端看到消息
注:发布的消息没有持久化,如果在订阅的客户端在第N次发布消息后订阅,那N次不能查看,只能收到订阅后第N+1次发布的消息
Redis新数据类型
Bitmaps
概念
- 现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图
- 合理地使用操作位能够有效地提高内存使用率和开发效率。
- Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
- Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
- Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
注意
- 很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。
- 在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞。
命令
- setbit<key><offset><value>设置Bitmaps中某个偏移量的值(0或1)
- getbit<key><offset>获取Bitmaps中某个偏移量的值
- bitcount<key>[start end] 统计字符串从start字节到end字节比特值为1的数量
- bitop and(or/not/xor) <destkey> [key…] bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。
##########################################Bitmap########################################
127.0.0.1:6379> setbit sign 0 1 # 周一 签到
(integer) 0
127.0.0.1:6379> setbit sign 1 0 # 周二 没签到
(integer) 0
127.0.0.1:6379> setbit sign 2 0 # 周三 没签到
(integer) 0
127.0.0.1:6379> setbit sign 3 1 # 周四 签到
(integer) 0
127.0.0.1:6379> getbit sign 2 # 获取周三状态
(integer) 0
127.0.0.1:6379> bitcount sign 0 -1 # 所有为1的总数
(integer) 2
小结:
类似z有序集合,但不是z
Bitmap 位图,数据结构!都是操作二进制位来进行记录,就只有0 和 1 两个状态!
HyperLogLog
概念
- 在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题
- 什么是基数?
- 比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
- 解决基数问题有很多种方案:
- 数据存储在MySQL表中,使用distinct count计算不重复个数
- 使用Redis提供的hash、set、bitmaps等数据结构来处理
- 以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
- 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
- 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
命令
- pfadd <key>< element> [element …] 添加指定元素到 HyperLogLog 中,如果执行命令后估计的近似基数发生变化,则返回1,否则返回0。
- fcount<key> [key …] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
- pfmerge<destkey><sourcekey> [sourcekey …] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
##########################################Hyperloglog########################################
127.0.0.1:6379> pfadd mykey a b c d e f g h i j # 添加一组元素
(integer) 1
127.0.0.1:6379> pfcount mykey # 统计一组元素
(integer) 10
127.0.0.1:6379> PFadd mykey2 i j z x c v b n m
(integer) 1
127.0.0.1:6379> pfcount mykey2
(integer) 9
127.0.0.1:6379> pfmerge mykey3 mykey mykey2 # 合并 mykey3 = mykey + mykey2 ,并且去重
OK
127.0.0.1:6379> pfcount mykey3
(integer) 15
小结:
跟s无序集合类似,但不是是s
网页的 UV (一个人访问一个网站多次,但是还是算作一个人!)
优点:占用的内存是固定,2/64不同的元素的技术,只需要废12KB内存!如果要从内存角度来比较的话 Hyperloglog首选!
0.81% 错误率! 如果允许容错,那么一定可以使用 Hyperloglog ! 如果不允许容错,就使用 set 或者自己的数据类型即可!
Geospatial
概念
- Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
命令
- Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
命令
- geoadd<key>< longitude><latitude><member> [longitude latitude member…] 添加地理位置(经度,纬度,名称)
- 有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出指定范围时,该命令将会返回一个错误。
- 已经添加的数据,是无法再次往里面添加的。
- 两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。
- geopos <key><member> [member…] 获得指定地区的坐标值
- geodist<key><member1><member2> [m|km|ft|mi ] 获取两个位置之间的直线距离
- m 表示单位为米[默认值]。
- km 表示单位为千米。
- mi 表示单位为英里。
- ft 表示单位为英尺。
- 如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位
- georadius<key>< longitude><latitude>radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素
##########################################Geospatial########################################
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai 120.15 30.28 hangzhou 113.27 23.13 gaungzhou # 添加单个/多个
(integer) 3
127.0.0.1:6379> zrange china:city 0 -1 # GEO底层的实现原理其实就是Zset!我们可以使用Zset命令来操作geo!
1) "gaungzhou"
2) "hangzhou"
3) "shanghai"
4) "beijing"
127.0.0.1:6379> geopos china:city beijing hangzhou # 获取指定的城市的经度和纬度
1) 1) "116.39999896287918091"
2) "39.90000009167092543"
2) 1) "120.15000075101852417"
2) "30.2800007575645509"
127.0.0.1:6379> geodist china:city shanghai hangzhou km # 两个member的距离,km为单位
"164.5694"
127.0.0.1:6379> georadius china:city 110 30 1000 km withdist withcoord count 1 georadius 以给定的经纬度为中心,找出某一半径内的元素,以110,30经纬度为中心,
1) 1) "gaungzhou"
2) "830.3533"
3) 1) "113.27000051736831665"
2) "23.13000101271457254"
127.0.0.1:6379> georadiusbymember china:city hangzhou 1000 km withdist withcoord # 以hangzhou为中心,寻找1000km的member ,withdist 显示两点的直线距离 withcoord 显示该点的经纬度
1) 1) "hangzhou"
2) "0.0000"
3) 1) "120.15000075101852417"
2) "30.2800007575645509"
2) 1) "shanghai"
2) "164.5694"
3) 1) "121.47000163793563843"
2) "31.22999903975783553"
小结:
底层是z,有序集合。
两点距离,附近的人
地理位置,有效的经度从-180度到180度。 有效的纬度从-85.05112878度到85.05112878度。当坐标位置超出上述指定范围时,该命令将会返回一个错误。
Redis_Jedis测试
概念
- Redis 官方推荐的 java连接开发工具! 使用Java 操作Redis 中间件!如果你要使用 java操作redis,那么一定要对Jedis 十分的熟悉!
Jedis所需要的jar包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
连接Redis注意事项
- redis.conf中注释掉bind 127.0.0.1 ,然后 protected-mode no
- 禁用Linux的防火墙:比如Linux(CentOS7)里执行命令,systemctl stop/disable firewalld.service
测试相关数据类型
public void f1(){
Jedis jedis = new Jedis("192.168.56.10", 6379);
System.out.println(jedis.ping());
}
public void f2(){
Jedis jedis = new Jedis("192.168.56.10", 6379);
jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.set("k3", "v3");
Set<String> keys = jedis.keys("*");
System.out.println(keys.size());
for (String key : keys) {
System.out.println(key);
}
// 3
//k3
//k1
//k2
}
public void f3(){
Jedis jedis = new Jedis("192.168.56.10", 6379);
jedis.lpush("mylist","aa");
jedis.lpush("mylist","bb");
List<String> list = jedis.lrange("mylist",0,-1);
for (String element : list) {
System.out.println(element);
}
// bb
//aa
}
Redis_Jedis实例
public class phone {
public static void main(String[] args) {
//模拟验证码发送
verifyCode(“111666”);
}
public static void getRedisCode(String phone,String code){
//连接redis
Jedis jedis=new Jedis("192.168.56.10",6379);
//验证码key
String codeKey="VerifyCode"+phone+":code";
String redisCode=jedis.get(codeKey);
//判断
if(redisCode.equals(code)){
System.out.println("success");
}else{
System.out.println("lose");
}
jedis.close();
}
//每个手机每天只能发送三次,验证码放到redis中,设置过期时间为120
public static void verifyCode(String phone,String code){
//连接redis
Jedis jedis=new Jedis("192.168.56.10",6379);
//拼接key
//手机发送次数key
String countKey="VerifyCode"+phone+":count";
//验证码key
String codeKey="VerifyCode"+phone+":code";
String count=jedis.get(countKey);
if(count==null){
jedis.setex(countKey,24*60*60,"1");
}else if(Integer.parseInt(count)<=2){
jedis.incr(countKey);
}else if(Integer.parseInt(count)>2){
System.out.println("超过三次");
jedis.close();
}
String vscode = getcode();
jedis.setex(codeKey,120,vscode);
jedis.close();
}
public static String getcode(){
Random random=new Random();
String code="";
for(int i=0;i<6;i++){
int rand = random.nextInt(10);
code+=rand;
}
return code;
}
}
Redis与Spring Boot整合
- 整合依赖文件
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
- redis配置文件
#Redis服务器地址
spring.redis.host=192.168.140.136
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
- redis配置类
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
- 测试代码
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
Redis_事务锁机制秒杀
Redis的事务定义
- Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- Redis事务的主要作用就是串联多个命令防止别的命令插队。
Multi、Exec、discard
- 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
- 组队的过程中可以通过discard来放弃组队
案例
- 案例
- 组队成功,提交成功
- 组队阶段报错,提交失败
- 组队成功,提交有成功有失败情况
事务的失败处理
- 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
- 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
事务冲突的问题
例子
悲观锁
- 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁
- 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
WATCH key [key …]
unwatch
- 取消 WATCH 命令对所有 key 的监视。
- 如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
- http://doc.redisfans.com/transaction/exec.html
Redis事务三特性
- 单独的隔离操作
- 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念
- 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
- 不保证原子性
- 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
Redis事务秒杀案例
秒杀并发模拟
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1 uid和prodid非空判断
if(uid == null || prodid == null) {
return false;
}
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3 拼接key
// 3.1 库存key
String kcKey = "sk:"+prodid+":qt";
// 3.2 秒杀成功用户key
String userKey = "sk:"+prodid+":user";
//4 获取库存,如果库存null,秒杀还没有开始
String kc = jedis.get(kcKey);
if(kc == null) {
System.out.println("秒杀还没有开始,请等待");
jedis.close();
return false;
}
// 5 判断用户是否重复秒杀操作
if(jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if(Integer.parseInt(kc)<=0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7.1 库存-1
jedis.decr(kcKey);
//7.2 把秒杀成功用户添加清单里面
jedis.sadd(userKey,uid);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
连接超时,通过连接池解决
ab并发安装
联网
- yum install httpd-tools
无网络-略
超卖问题-模拟
ab测试
- vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。
- 内容:prodid=0101&
- ab -n 2000 -c 200 -k -p ./postfile -T application/x-www-form-urlencoded http://192.168.3.157:8080/Seckill/doseckill
超卖展示
超卖问题-解决1-乐观锁
- 利用乐观锁淘汰用户,解决超卖问题。
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1 uid和prodid非空判断
if(uid == null || prodid == null) {
return false;
}
//2 连接redis
//Jedis jedis = new Jedis("192.168.44.168",6379);
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3 拼接key
// 3.1 库存key
String kcKey = "sk:"+prodid+":qt";
// 3.2 秒杀成功用户key
String userKey = "sk:"+prodid+":user";
//监视库存
jedis.watch(kcKey);
//4 获取库存,如果库存null,秒杀还没有开始
String kc = jedis.get(kcKey);
if(kc == null) {
System.out.println("秒杀还没有开始,请等待");
jedis.close();
return false;
}
// 5 判断用户是否重复秒杀操作
if(jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if(Integer.parseInt(kc)<=0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7 秒杀过程
// //使用事务
Transaction multi = jedis.multi();
//组队操作
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List<Object> results = multi.exec();
if(results == null || results.size()==0) {
System.out.println("秒杀失败了....");
jedis.close();
return false;
}
// //7.1 库存-1
// jedis.decr(kcKey);
// //7.2 把秒杀成功用户添加清单里面
// jedis.sadd(userKey,uid);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
库存遗留问题
- 已经秒光,可是还有库存。原因,就是乐观锁导致很多请求都失败。先点的没秒到,后点的可能秒到了。
LUA脚本
- Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
- 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
- LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
- 但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
库存遗留-解决2
-
解决超卖问题
-
库存遗留问题
-
lua脚本
local userid=KEYS[1];
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr';
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then
return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",qtkey);
redis.call("sadd",usersKey,userid);
end
return 1;
- 代码
static String secKillScript ="local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":usr\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1" ;
static String secKillScript2 =
"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
" return 1";
public static boolean doSecKill(String uid,String prodid) throws IOException {
JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis=jedispool.getResource();
//String sha1= .secKillScript;
String sha1= jedis.scriptLoad(secKillScript);
Object result= jedis.evalsha(sha1, 2, uid,prodid);
String reString=String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}
- 结果
总结
- 老师点10次,正常秒杀
- 使用工具ab模拟并发测试,会出现超卖情况。查看库存会出现负数。
- 第二版:加事务-乐观锁(解决超卖),但出现遗留库存和连接超时
- 第三版:连接池解决超时问题
- 第四版:解决库存依赖问题,LUA脚本
Redis持久化之RDB
概念
- 在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
- 优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
备份是如何执行的
- Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork
- Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
- 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
配置文件
################################ 快照 #################################
#
# Save the DB on disk:保存数据库到磁盘
#
# save <秒> <更新>
#
# 如果指定的秒数和数据库写操作次数都满足了就将数据库保存。
#
# 下面是保存操作的实例:
# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化)
# 300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化)
# 60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)
#
# 注释:注释掉“save”这一行配置项就可以让保存数据库功能失效。
#
# 你也可以通过增加一个只有一个空字符串的配置项(如下面的实例)来去掉前面的“save”配置。
#
# save ""
save 900 1
save 30 10
save 60 10000
#在默认情况下,如果RDB快照持久化操作被激活(至少一个条件被激活)并且持久化操作失败,Redis则会停止接受更新操作。
#这样会让用户了解到数据没有被正确的存储到磁盘上。否则没人会注意到这个问题,可能会造成灾难。
#
#如果后台存储(持久化)操作进程再次工作,Redis会自动允许更新操作。
#
#然而,如果你已经恰当的配置了对Redis服务器的监视和备份,你也许想关掉这项功能。
#如此一来即使后台保存操作出错,redis也仍然可以继续像平常一样工作。
stop-writes-on-bgsave-error yes
#是否在导出.rdb数据库文件的时候采用LZF压缩字符串和对象?
#默认情况下总是设置成‘yes’, 他看起来是一把双刃剑。
#如果你想在存储的子进程中节省一些CPU就设置成'no',
#但是这样如果你的kye/value是可压缩的,你的到处数据接就会很大。
rdbcompression yes
#从版本RDB版本5开始,一个CRC64的校验就被放在了文件末尾。
#这会让格式更加耐攻击,但是当存储或者加载rbd文件的时候会有一个10%左右的性能下降,
#所以,为了达到性能的最大化,你可以关掉这个配置项。
#
#没有校验的RDB文件会有一个0校验位,来告诉加载代码跳过校验检查。
rdbchecksum yes
# 导出数据库的文件名称
dbfilename dump.rdb
# 工作目录
#
# 导出的数据库会被写入这个目录,文件名就是上面'dbfilename'配置项指定的文件名。
#
# 只增的文件也会在这个目录创建(这句话没看明白)
#
# 注意你一定要在这个配置一个工作目录,而不是文件名称。
dir ./
save 和 bgsave区别
概念
- save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。
- bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
- 可以通过lastsave 命令获取最后一次成功执行快照的时间
- 注意: bgsave命令是针对save阻塞问题做的优化。 推荐Redis内部所有涉及到RDB操作都采用bgsave的方式。
save
127.0.0.1:6379> set user n1
OK
127.0.0.1:6379> save
OK
bgsave
[root@iZbp143t3oxhfc3ar7jey0Z redis-4.0.12]# redis-cli
127.0.0.1:6379> save
OK
127.0.0.1:6379> set addr bj
OK
127.0.0.1:6379> BGSAVE
Background saving started
rdb的备份
Redis持久化之AOF
概念
- 以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
AOF持久化流程
- 客户端的请求写命令会被append追加到AOF缓冲区内;
- AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
- AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
- Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
配置文件
-
aof默认是关闭的, 如果想要打开, 也是在redis的配置文件redis.conf中进行配置, 如果想要打开, 则可以在配置文件中查找appendonly, 修改为如下:
- appendonly yes
- 生产环境下一般都是打开的;
-
aof刷新的时间间隔配置appendfsync, 有三种配置方式:
- always: 每一条都写入磁盘, 对数据进行保存, 对性能影响较大;
- everysec: 每秒写入磁盘一次, 对性能影响较小;
- no: 不进行设置, 让操作系统自动保存;
-
aof的rewrite过程:
- redis创建一个子进程;
- 子进程根据当前redis的数据, 重新写一份aof文件;
- 与此同时, redis的主进程继续接受写入的数据并写入内存中的日志, 同时新的日志也会追加到旧的aof文件中;
- 子进程写完新的aof文件之后, redis会将内存中的新数据的日志追加到新的aof文件中;
- 最后用新的aof文件替换旧的aof文件;
-
rewrite操作的配置
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
- 第一个表示: aof文件的容量超过原来aof文件容量一倍的时候, 进行aof文件的重写;
- 第二个表示: 执行aof重写时, aof文件的最小容量;
AOF启动/修复
-
AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
-
正常启动
- 修改默认的appendonly no,改为yes
- 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)
-
异常恢复
- 修改默认的appendonly no,改为yes
- 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof–fix appendonly.aof进行恢复
- 备份被写坏的AOF文件
- 恢复:重启redis,然后重新加载
AOF同步频率设置
- appendfsync always
- 始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
- appendfsync everysec
- 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
- appendfsync no
- redis不主动进行同步,把同步时机交给操作系统。
Rewrite压缩
概念
- AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof
重写原理
- AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
- no-appendfsync-on-rewrite:
- 如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
- 如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
如何实现重写
- 重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
- auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
- auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
- 如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
重写流程
- bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
- 主进程fork出子进程执行重写操作,保证主进程不会阻塞。
- 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
- 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
- 主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
- 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
优势
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
劣势
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
- 存在个别Bug,造成恢复不能。
总结
- AOF和RDB同时开启,redis听谁的?
- AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
- 用哪个好
- 官方推荐两个都启用。
- 如果对数据不敏感,可以选单独用RDB。
- 不建议单独用 AOF,因为可能会出现Bug。
- 如果只是做纯内存缓存,可以都不用。
官网建议
- RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
- AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.
- Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
- 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
- 同时开启两种持久化方式
- 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
- RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?
- 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
- 性能建议
- 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。
- 如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。
- 代价
- 一是带来了持续的IO
- 二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。
- 只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。
- 代价
Redis_主从复制
概念
- 主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
- 作用
- 读写分离,性能扩展
- 容灾快速恢复
实践
-
同一个服务器的话,启动不同的端口,需要三个conf,
-
启动三台redis服务器
-
查看三台主机运行情况
info replication
- 配从(库)不配主(库)
slaveof <ip><port>
# slaveof no one 清除主从
-
效果展示
-
可以将配置增加到文件中,才能是永久生效。
常用3招
一主二仆
-
从机不可写
-
主机shutdown
- 主机挂掉,重启就行,一切如初
-
从机shutdown
- 从机重启需重设:slaveof 127.0.0.1 6379(配置文件永久)
薪火相传
- 上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。
- 风险是一旦某个slave宕机,后面的slave都没法备份。主机挂了,从机还是从机,无法写数据了
反客为主
- 当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。
- 用 slaveof no one 将从机变为主机。
复制原理
- Slave启动成功连接到master后会发送一个sync命令
- Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
- 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
哨兵模式(sentinel)
概念
- 反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主机
实践
- 自定义的/myredis目录下新建sentinel.conf文件(名字绝不能错)
- 配置哨兵,填写内容
// 其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
sentinel monitor mymaster 127.0.0.1 6379 1
- 启动哨兵
redis-sentinel /myredis/sentinel.conf
- 结果
复制延时
- 由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
故障恢复
- 优先级在redis.conf中默认:slave-priority 100,值越小优先级越高(后面由于政治原因,slave-priority改成了replica-priority)
- 偏移量是指获得原主机数据最全的
- 每个redis实例启动后都会随机生成一个40位的runid
主从复制-代码
private static JedisSentinelPool jedisSentinelPool=null;
public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
Set<String> sentinelSet=new HashSet<>();
sentinelSet.add("192.168.11.103:26379");
JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
}
}
Redis集群
问题
- 容量不够,redis如何进行扩容?
- 并发写操作, redis如何分摊?
- 另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。
- 之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。
概念
- Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
- Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
实践
- 删除持久化数据,将rdb和aof文件都删除掉。
- 配置文件
include ./redis.conf
port 6379
pidfile "./redis_6379.pid"
dbfilename "dump6379.rdb"
dir ./
logfile ./redis_err_6379.log
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
- 服务全部启动,确保nodes-xxxx.conf文件都生成正常
感觉nodes-xxxx.conf应该写死路径,懒得改了
- 合体:
- 5.0+版本不用另外安装ruby,旧的可能需要另外安装
- 尽量不要用127.0.0.1代替本机,有bug
- –replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。
- 分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。
redis-cli --cluster create --cluster-replicas 1 10.0.2.15:6379 10.0.2.15:6380 10.0.2.15:6381 10.0.2.15:6382 10.0.2.15:6383 10.0.2.15:6384
- 登录客户端
- 不要普通模式登录(设置的key不在当前卡槽会报错)
- -c 采用集群策略连接,设置数据会自动切换到相应的写主机
- cluster nodes 命令查看集群信息
什么是slots
- 一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个。
- 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
- 集群中的每个节点负责处理一部分插槽
操作
录入
- 根据key的值来自动切换卡槽(集群节点)
- 不在一个slot下的键值,是不能使用mget,mset等多键操作。
- 可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。
查询集群中的值
- cluster keyslot key,查询key所在slot
- cluster countkeysinslot slot,查询slot的key个数
- cluster getkeysinslot slot,查询slot的key
故障恢复
-
主节点下线
- 当前的从节点能自动升为主节点
-
主节点恢复后
- 主节点回来变成从机(反客为主)
-
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?看redis.conf中的参数 cluster-require-full-coverage:
- cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉
- cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。
集群的Jedis开发
- 即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。
- 无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据。
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort>set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.31.211",6379)); // 随便一个集群就行,反正无中心化
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}
Redis 集群总结
- 好处
- 实现扩容
- 分摊压力
- 无中心配置相对简单
- 坏处
- 多键操作是不被支持的
- 多键的Redis事务是不被支持的。lua脚本不被支持
- 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。
Redis应用问题解决
缓存穿透
问题描述
- key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
- -透,透明,数据库不存在的值
解决方案
- 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟
- 设置可访问的名单(白名单):
- 使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
- 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。
- 布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
- 进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
缓存击穿
问题描述
- key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
- 预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
- 实时调整:现场监控哪些数据热门,实时调整key的过期时长
- 使用锁:
- 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
- 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
- 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
- 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
缓存雪崩
问题描述
- key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
- 缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key
解决方案
- 构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)
- 使用锁或队列:
- 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
- 设置过期标志更新缓存:
- 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
- 将缓存失效时间分散开:
- 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
分布式锁
问题描述
-
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
-
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等)(性能:redis最高)
- 基于Zookeeper(可靠性:zookeeper最高)
解决方案:使用redis实现分布式锁
set sku:1:info "OK" NX PX 10000
- ex second :设置键的过期时间为 second 秒。 set key value ex second 效果等同于 setex key second value 。
- px millisecond :设置键的过期时间为 millisecond 毫秒。 set key value px millisecond 效果等同于 psetex key millisecond value 。
- nx :只在键不存在时,才对键进行设置操作。 set key value nx 效果等同于 setnx key value 。
- xx :只在键已经存在时,才对键进行设置操作。
图解
- 多个客户端同时获取锁(setnx)
- 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)
- 其他客户端等待重试
编写代码
@GetMapping("testLock")
public void testLock(){
//1获取锁,setne
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
redisTemplate.delete("lock");
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Redis: set num 0
// 重启,服务集群,通过网关压力测试:
ab -n 1000 -c 100 http://192.168.140.1:8080/test/testLock
问题
- setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放。
- 解决:设置过期时间,自动释放锁。
优化之设置锁的过期时间
-
设置过期时间有两种方式:
- 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
- 在set时指定过期时间(推荐)
问题
- 可能会释放其他服务器的锁。
- 场景:如果业务逻辑的执行时间是7s。执行流程如下
- index1业务逻辑没执行完,3秒后锁被自动释放。
- index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
- index3获取到锁,执行业务逻辑
- .index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。
- 最终等于没锁的情况。
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
优化之UUID防误删
问题
- 删除操作缺乏原子性。
- 场景:
优化之LUA脚本保证删除的原子性
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String skuId = "25"; // 访问skuId 为25号的商品 100008348542
String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Redis6.0新功能
ACL
概念
- 参考官网:https://redis.io/topics/acl
- Redis ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。
- 在Redis 5版本之前,Redis 安全规则只有密码控制 还有通过rename 来调整高危命令比如 flushdb , KEYS* , shutdown 等。Redis 6 则提供ACL的功能对用户进行更细粒度的权限控制 :
- 接入权限:用户名和密码
- 可以执行的命令
- 可以操作的 KEY
命令
-
使用acl list命令展现用户权限列表
-
使用acl cat命令
- 查看添加权限指令类别
- 加参数类型名可以查看类型下具体命令
- 查看添加权限指令类别
-
使用acl whoami命令查看当前用户
-
使用aclsetuser命令创建和编辑用户ACL
-
ACL规则。下面是有效ACL规则的列表。某些规则只是用于激活或删除标志,或对用户ACL执行给定更改的单个单词。其他规则是字符前缀,它们与命令或类别名称、键模式等连接在一起。
-
通过命令创建新用户默认权限:accl setuser user1(如果用户不存在,这将使用just created的默认属性来创建用户。如果用户已经存在,则上面的命令将不执行任何操作。)
-
设置有用户名、密码、ACL权限、并启用的用户:acl setuser user2 on >password ~cached:* +get
-
切换用户,验证权限
-
IO多线程
概念
- Redis6终于支撑多线程了,告别单线程了吗?
- IO多线程其实指客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程。
原理架构
- Redis 6 加入多线程,但跟 Memcached 这种从 IO处理到数据访问多线程的实现模式有些差异。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。整体的设计大体如下:
- 另外,多线程IO默认也是不开启的,需要再配置文件中配置
io-threads-do-reads yes
io-threads 4
工具支持 Cluster
- 之前老版Redis想要搭集群需要单独安装ruby环境,Redis 5 将 redis-trib.rb 的功能集成到 redis-cli 。另外官方 redis-benchmark 工具开始支持 cluster 模式了,通过多线程的方式对多个分片进行压测。
Redis新功能持续关注
- RESP3新的 Redis 通信协议:优化服务端与客户端之间通信
- Client side caching客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。
- Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。
- Modules API
- Redis 6中模块API开发进展非常大,因为Redis Labs为了开发复杂的功能,从一开始就用上Redis模块。Redis可以变成一个框架,利用Modules来构建不同系统,而不需要从头开始写然后还要BSD许可。Redis一开始就是一个向编写各种系统开放的平台。