NoSQL数据库
1.NoSQL的优点(为什么要使用NoSQL)
-
为了解决CPU访问压力过大的问题
session怎么办?在分布式集群的场景下,session具体存储在哪台服务器上?
综合几个解决方案的优缺点,我们选择使用缓存处理器 -
解决数据库的访问压力过大的问题
减少io的读操作,从内存读取大大的提升了读取速度,还避免了读取错误的问题
2.什么是NoSQL?
非关系型数据库,内部由key-value方式存储的
不支持SQL标准
不支持ACID
远超过SQL的性能
3.NoSQL的使用场景
- 适用的场景
数据的高并发
海量数据的读写
数据高可扩展性的情景 - 不适用的场景
需要事务支持的情况下
基于sql的结构化查询
Redis的安装与运行
1. Redis的安装
Redisredis官网首页提供redis最新的安装包提供下载
需要在Linux系统中进行安装
- 系统需要具备gcc编译环境
- 解压缩redis安装压缩包
- 执行make命令
- 执行make install 命令
2. Redis的运行
推荐使用后台运行
cd ~/server/redis-6.2.6
sudo cp redis.conf /etc/redis.conf
cd /etc
sudo gedit redis.conf
在打开的文档中搜索daemonize no 将no改为yes
cd /usr/local/bin
redis-server /etc/redis.conf
至此完成了redis的启动
之后我们调用ps命令,可以查看到redis已经运行起来了
ps -ef | grep redis
之后我们调用该命令可以进入redis进行操作
3.Redis的关闭
我们可以在进入redis之后使用shutdown的命令关闭
也可以exit跳出redis后,kill吊redis的进程号
kill -9 24105
4. Redis相关知识介绍
- 可以通过select 1|2|3 来选择库,默认为0
- redis中使用单线程+多路IO复用
类似于这个图中讲的,买票的操作只通过黄牛来进行,而发出买票操作消息的人可以有多个,这些人告诉黄牛买票信息之后就可以干别的事情去了,而不会一直等待票的产生
3. 支持持久化
4. 支持多数据类型
5. 常见错误及解决办法
-
解决办法
解决—MISCONF Redis被配置为保存RDB快照,但目前无法在磁盘上存留。可能修改数据集的命令被禁用。请检查Redis日志,了解有关错误的详细信息。 - Mr_Yun - 博客园 (cnblogs.com)
在redis启动后,输入config set stop-writes-on-bgsave-error no
-
idea中连接一段时间后自动断连,导致程序第一次连接显示连接失败,第二次连接才连接成功
此时需要将redis.conf文件中的bind行内容修改为
bind x.x.x.x ::1 x指的是虚拟机的ip地址
重启redis
但此时redis中使用redis-clip无法进入redis
需要使用 redis-cli -h x.x.x.x -p 6379命令进入redis -
报MISCONF Redis is configured to save RDB snapshots, but it is currently not able… 错误
解决办法
(2条消息) 解决redis连接错误:MISCONF Redis is configured to save RDB snapshots,即redis无法向磁盘写入RDB的报错_小白_努力-CSDN博客
可以进入redis之后输入config set stop-writes-on-bgsave-error no
命令
也可以通过修改redis.conf文件
vi打开redis-server配置的redis.conf文件,然后使用快捷匹配模式:/ stop-writes-on-bgsave-error定位到stop-writes-on-bgsave-error字符串所在位置,接着把后面的yes设置为no
6.相关工具的安装
ab压力测试工具安装,在命令行输入
sudo apt-get install apache2-utils
Redis常用数据类型
1. Redis键(key)
-
keys* 查看当前库所有key
-
exists key判断某个key是否存在
-
type key查看key是什么类型
-
del key删除指定的key数据
-
unlink key 根据value选择非阻塞删除
仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作.
expire key 10 10秒钟:为给定的可以设置过期时间
ttl key 查看还有多少秒过期, -1表示永不过期,-2表示已过期 -
select 命令切换库
-
dbsize 查看当前数据库的key的数量
-
flushdb 清空当前库
-
flushall 通杀全部库
2.Redis字符串(String)
- String 是redis中最常用的数据类型,一个redis中字符串value最多可以是512M
2.1 String 基本操作
- set <key> <value> :设置值
- get <key> :取值
- append <key> <value>:在某个key之后追加值
- strlen <key>:获得值的长度
- setnx <key><value>:只有在key不存在时设置key的值
- incr<key>:将key中存储的数字值增1
只能对数字进行操作,如果为空,新增值为1 - decr<key>:将key中存储的数字值减1
只能对数字进行操作,如果为空,新增值为-1
由于Redis操作是单线程的,因此其满足原子性,所以这里不涉及多线程情况下incr与decr出问题的情况 - mset<key1><value1><key2><value2>
同时设置一个或多个key-value对 - mget<key1><key2>
同时获取一个或多个value - msetnx<key1><value1><key2><value2>
同时设置一个或多个key-value对,当且仅当这些key不存在,有一个失败则都失败 - getrange <key><起始位置><结束位置>
获得值的范围,类似java的substring,前包含,后包含 - setrange <key><起始位置><value>
用value覆写key所存储的字符串值,从起始位置开始 - setex <key><过期时间><value>
设置键值的同时,设置过期的时间,单位秒 - getset <key><value>
以旧换新,设置新值的同时获得旧值
2.2数据结构
类似于Stringbuffer
3.Redis列表(List)
redis列表是简单的字符串列表,按照插入顺序排序.可以添加一个元素到列表的头部或这尾部
实际底层是一个双向链表,对首位两端的操作性能高,通过索引下标的操作中间节点的性能较差
3.1 常用命令
- lpush/rpush <key><value1><value2><value3> 在左边/右边插入一个或多汁
- lpop/rpop <key> 从左边/右边弹出一个值.值在键在,值光键光
- lrange <key> <start> <stop> 按照索引的下标获得元素
- lindex <key> <index> 按照索引下标获得元素
- llen <key> 获得列表长度
- linsert <key> before/after <value><newvalue> 在<value>的前面/后面插入<newvalue>
- lrem <key><n><value> 从左边删除n个value(从左到右)
- lset<key><index><value> 将列表key下标为index的值替换为value
3.2 数据结构
底层数据结构为quicklist
当元素很少时,使用一个压缩链表就可以存储了,当数据很多时,使用多个压缩链表存储,并将这些压缩链表再链接起来,构成一整个链表
4.Redis集合(Set)
底层为一个hash表
4.1 常用命令
- sadd <key><value1><value2>…将一个或多个元素加入到集合key中,已经存在的元素会被忽略
- smembers <key> 取出该集合的所有值
- sismember <key><value> 判断集合<key>是否为含有该<value>的值,有1,没有0
- scard <key>返回该集合的元素个数
- srem <key><value1><value2>…删除集合中的某个元素
- spop<key>随机从该集合中弹出一个值
- srandmemeber <key><n>随机从该集合中取出n个值.不会从集合中删除
- smove<source><desination> value 把集合中的一个值从一个集合移动到另一个集合
- sinter <key1> <key2> 返回两个集合的交集元素
- sunion <key1> <key2> 返回两个集合的并集元素
- sdiff <key1> <key2> 返回两个集合的差集 元素(key1中的不包含key2的)
4.2 数据结构
通过hash表实现的dict
5.Redis哈希(Hash)
Redis hash 是一个String类型的field和value的映射表,hash特别适合用于存储对象类似于java里面的Map<String,Object>
常用命令
- hset <key><field><value> 给<key>集合中<field>键赋值<value>
- hget <key><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不存在
6.Redis有序集合Zset(sorted set)
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分( score),这个评分( score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了。|
因为元素是有序的,所以你也可以很快的根据评分( score )或者次序( position )来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
常用命令
- zadd <key><score1><value1><score2><value2>…
将一个或多个member元素及其score值加入到有序集key当中。 - zrange key start stop [WITHSCORES]
返回有序集key 中,下标在<start><stop>之间的元素
带WITHSCORES,可以让分数一起和值返回到结果集。 - zrangebyscore key min max [withscores] [limit offset count]
返回有序集key中,所有score值介于min和max 之间(包括等于min或max )的成员。有序集成员按score值递增(从小到大)次序排列。 - zrevrangebyscore key max min [withscores] [limit offset count]
同上,改为从大到小排列。 - zincrby <key><increment><value>为元素的score加上增量
- zrem <key><value>删除该集合下,指定值的元素,
- zcount <key><min><max>统计该集合,分数区间内的元素个数。
- zrank <key><value>返回该值在集合中的排名,从0开始。
数据结构
SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String,Double>,可以给每一个元素value赋予一个权重scorei另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
zset 底层使用了两个数据结构。
( 1 ) hash , hash的作用就是关联元素value和权重score,保障元素value的唯—性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
配置文件
1.Units 单位
配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit,大小写不敏感
2.Include
################################## INCLUDES ###################################
# Include one or more other config files here. This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings. Include files can include
# other files, so use this wisely.
#
# Note that option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf
3. Network
bind 配置:如果是127.0.0.1表示只能通过本地连接,如果想要远程连接,则需要注释调这句话,同时在下面找到
protected-mode yes
将yes改为no,让redis支持远程访问,修改完之后一定要重启redis,先杀掉进程再重新启动
tcp-keepalive 300 检测心跳机制,300指的是300s,每300s检测是否连接
发布和订阅
1. 什么是发布和订阅
Redis 发布订阅(pub/sub)是一种消息通信模式∶发送者(pub)发送消息,订阅者(sub)接收消息。
Redis客户端可以订阅任意数量的频道。
2.发布订阅命令行实现
Redis6 新数据类型
1.Bitmaps
Redis提供了Bitmaps这个“数据类型”可以实现对位的操作∶
( 1 )Bitmaps本身不是一种数据类型,实际上它就是字符串( key-value )但是它可以对字符串的位进行操作。
( 2 ) Bitmaps单独提供了一套命令,所以在Redis 中使用Bitmaps和使用字符串的方法不太相同。可以把 Bitmaps想象成一个以位为单位的数组数组的每个单元只能存储0和1,数组的下标在Bitmaps 中叫做偏移量
bitmap相较于set可以极大的减少存储空间
命令
-
setbit<key><offset><value>设置Bitmaps中某个偏移量的值(0或1)
每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记做1,没有访问的用户记做0,用偏移量作为用户的id。
设置键的第offset个位的值(从О算起),假设现在有20个用户,userid=1 ,6, 11, 15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图.
-
getbit<key><offset>获取Bitmaps中某个偏移量的值
-
bitcount
统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start或end参数,可以让计数只在特定的位上进行。start 和end参数的设置,都可以使用负数值:比如-1表示最后一个位,而-2表示倒数第二个位,start、end是指bit组的字节的下标数,二者皆包含。
bitcount <key> <start> <end> -
bitop and(or/not/xor) <destkey> [key…]
bitop是一个复合操作,它可以做多个Bitmaps的and (交集) 、 or (并集) 、 not(非) 、 xor (异或)操作并将结果保存在destkey中。
2.HyperLogLog
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV( PageView页面访问量) ,可以使用Redis的incr、incrby轻松实现。
但像UV ( UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
(1) 数据存储在MySQL表中,使用distinct count计算不重复个数
(2) 使用Redis提供的 hash、set、bitmaps等数据结构来处理,
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog
Redis HyperLogLog是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis里面,每个HyperLogLog键只需要花费12 KB内存,就可以计算接近2^64个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集{1,3,5,7,5,7,8},那么这个数据集的基数集为{1,3,5,7,8},基数(不重复元素)为5。基数估计就是在误差可接受的范围内,快速计算基数。
命令
- pfadd <key><element> [element …]添加指定元素到HyperLogLog 中
- pfcount<key> [key …]计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
- pfmerge<destkey><sourcekey> [sourcekey …]将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
3.Geospatial
Redis 3.2中增加了对GEO类型的支持。GEO ,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis 基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash 等常见操作。
命令
- geoadd<key><longitude><latitude><member> [longitude latitude member…]添加地理位置(经度,纬度,名称)
两极无法直接添加,一般会下载城市数据,直接通过Java程序一次性导入。有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。”
当坐标位置超出指定范围时,该命令将会返回一个错误。已经添加的数据,是无法再次往里面添加的。 - geopos<key><member> [member…]获得指定地区的坐标值
- geodist<key><member1><member2>[m|km|ft|mi ]获取两个位置之间的直线距离
m表示单位为米[默认值]。
vkm表示单位为千米。
vmi表示单位为英里。
ft表示单位为英尺。 - georadius<key><longitude><latitude>radius m|km|ft|mi以给定的经纬度为中心找出某一半径内的元素
Jedis
1.Jedis 依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
2.demo
public class JedisDemo1 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.52.128",6379);
String value = jedis.ping();
System.out.println(value);
}
@Test
public void demo1(){
Jedis jedis = new Jedis("192.168.52.128",6379);
jedis.set("name","lucy");
//获取
String name = jedis.get("name");
System.out.println(name);
//设置多个key-value
jedis.mset("k1","v1","k2","v2");
List<String> mget = jedis.mget("k1", "k2");
System.out.println(mget);
Set<String> keys = jedis.keys("*");
for(String key:keys){
System.out.println(key);
}
}
@Test
public void demo2(){
Jedis jedis = new Jedis("192.168.52.128",6379);
Long lpush = jedis.lpush("key1", "lucy", "marry", "jack");
List<String> key1 = jedis.lrange("key1", 0, -1);
System.out.println(key1);
}
@Test
public void demo3(){
Jedis jedis = new Jedis("192.168.52.128",6379);
jedis.sadd("name1","lucy","jack");
Set<String> name1 = jedis.smembers("name1");
System.out.println(name1);
}
@Test
public void demo4(){
Jedis jedis = new Jedis("192.168.52.128",6379);
jedis.hset("users","age","20");
String hget = jedis.hget("users", "age");
System.out.println(hget);
}
@Test
public void demo5(){
Jedis jedis = new Jedis("192.168.52.128",6379);
jedis.zadd("china",100d,"shanghai");
List<String> china = jedis.zrange("china", 0, -1);
System.out.println(china);
}
3.Jedis实例-手机验证码
要求:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败·
3、每个手机易每天只能输入3次
package com.zjm.jedis;
import redis.clients.jedis.Jedis;
import java.util.Random;
public class PhoneCode {
public static void main(String[] args) {
//模拟验证码的发送
verifyCode("13314311111");
}
//1.生成6为数字验证码
public static String getCode(){
Random random = new Random();
StringBuilder rescode = new StringBuilder();
for(int i=0;i<6;i++){
rescode.append(random.nextInt(10));
}
return rescode.toString();
}
//2.每个手机只能发送三次,验证码放到redis中,设置过期时间
public static void verifyCode(String phone){
//连接redis
Jedis jedis = new Jedis("192.168.52.128", 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("今天发送次数超过了3次");
jedis.close();
return;
}
//发送验证码放到redis中
String vcode = getCode();
jedis.setex(codeKey,120,vcode);
jedis.close();
}
//3.验证码校验
public static void getRedisCode(String phone,String code){
//从redis获取验证码
Jedis jedis = new Jedis("192.168.52.128", 6379);
String codeKey = "VerifyCode"+phone+":code";
String vcode = jedis.get(codeKey);
if(vcode.equals(code)) {
System.out.println("成功");
}else{
System.out.println("失败");
}
jedis.close();
}
}
SpringBoot 与Redis之间的整合
1. 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>
2.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.poo1.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.poo1.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.poo1.min-idle=0
#yml中
spring:
redis:
host: 192.168.52.128
port: 6379
database: 0
timeout: 1800000
lettuce:
pool:
max-active: 20
max-wait: -1
max-idle: 5
min-idle: 0
3. config文件的编写
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
// @Bean
// public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
// RedisTemplate<String, Object> template = new RedisTemplate<>();
// StringRedisSerializer 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);
// template.setKeySerializer(redisSerializer);
// return template;
// }
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
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);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
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))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
Demo
@RestController
@RequestMapping("/redis")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/test1")
public String testRedis(){
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
Redis 事务 锁机制 秒杀
1.事务
Redis事务是一个单独的隔离操作∶事务中的所有命令都会序列化、按顺序也执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令 防止别的命令插队。
基本命令
Multi,Exec,Discard
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队的过程中可以通过discard来放弃组队。 “
错误的两种情况
- 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
- 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令执行
事务冲突的问题
解决办法
-
悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
-
乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
redis中乐观锁的应用
命令 WATCH key [key …]
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断 。
我们在两个redis窗口中执行事务,让他们都监视balance,都执行事务,对balance操作,上方窗口先执行,执行完毕后下方窗口执行失败
Redis事务的三特性
- 单独的隔离操作。
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 - 没有隔离级别的概念。
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行。 - 不保证原子性。
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
2. 秒杀过程
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.52.128", 6379);
//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.sismemeber(useKey,uid)){
System.out.println("已经秒杀成功了,不能重复秒杀");
Jedis.close();
return false;
}
//6.判断如果商品数量,库存数量<1,秒杀结束
if(Integer.parseInt(kc)<=0){
System.out.println("秒杀已经结束了");
Jedis.close();
return false;
}
//7.秒杀过程
//7.1库存-1
//使用事务
Transaction multi=jedis.multi();
//组队操作(库存-1,用户加到清单里)
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;
}
/**
jedis.decr(kcKey);
//7.2把秒杀成功用户加到清单里面
jedis.sadd(userKey,uid);*/
System.out.println("秒杀成功");
jedis.close();
return true;
}
ab测试
1000个请求,100个并发
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.2.115:8080/sckill/doseckill
连接超时问题
通过jedis连接池来实现
超卖问题
通过添加乐观锁
库存遗留问题
使用Lua脚本
将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成—些redis事务性的操作。
但是注意redis的lua脚本功能,只有在Redis 2.6以上的l版本才可以使用。
利用lua脚本淘汰用户,解决超卖问题。
redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis利用其单线程的特性,用任务队列的方式解决多任务并发问题。·
local userid = KEYS[1];
local prodid = KEYS[2];
local qtkey = "sk:"..prodid..":qt";
local usersKey = "sk:"..prodid..":usr";
local userExists = redis.call("sismemeber",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;
I
Redis 持久化之RDB
在指定的时间间隔将数据存储到硬盘中
将数据先存放到临时文件中,保存完毕后再将临时文件放到rdb中,这样做的目的是保证数据的完整性
redis的持久化存储文件dump.rdb,存储路径放到了redis运行路径
在redis.conf文件中save字段可以修改redis持久化RDB的存储策略与周期
Redis是如何备份的
- Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
- 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术"
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
劣势
- Fork的的候,内存中的数据被克隆了一份,大致2倍的膨胀性需要 虑
- 虽然 Redis在 fok 时使用了写时拷贝技术,但是如果数据庞大时还是比绞消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果 Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
Redis持久化之AOF
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录) ,只许追加文件但不可以改写文件 ,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF开启
AOF默认是不开启的
可以在redis.conf中配置文件名称,默认为appendonly.aof将no改为yes
AOF文件的保存路径,同RDB的路径一致
AOF与RDB都开启,Redis默认取AOF的数据
aof文件的修复
如果文件数据脏了
使用redis-check-aof --fix appendonly.aof
AOF 同步频率设置
- appendfsync alwaysu
始终同步,每次 Redis 的写入都会立刻记入日志;性能较差但数据完整性化较好; - appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。 - appendfsync no
redis不主动进行同步,把同步时机交给操作系统。
rewrite 压缩
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令 bgrewriteaof
2重写原理,如何实现重写
AOF文件持续增长而过大时,会fork 出一条新进程来将文件重写(也是先写临时文件最后再rename) , redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作.
no-appendfsync-on-rewrite :
如果no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高生能)·
如果no-appendfsync-on-rewrite=no,还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)·
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的因此设定 Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)·
Redis 会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次 rewrite后大小的一倍且文件大于64M时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定 Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size :设置重写的基准值,最小文件64MB。达到这个值开始重写。
例如∶文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB.
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
如果Redis的 AOF当前大小>= base_size +base_size* 100%(默认)且当前大
3、重写流程
( 1 ) bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。·
( 2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis 内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)
1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
2).主进程把aof_rewrite_buf 中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写.
AOF持久化流程
( 1 )客户端的请求写命令会被append追加到AOF缓冲区内;
( 2 )AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
( 3 )AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
( 4 ) Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
优势
劣势
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
- 存在个别 Bug,造成恢复不能。
主从复制
主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master 以写为主,Slave以读为主:
优点
- 读写分离
- 容灾快速恢复
主从复制模拟(一主两从)
-
创建/myRedis文件夹
-
复制redis.conf配置文件到文件夹中
-
配置一主两从,创建三个配置文件
redis6379.conf
redis6380.conf
redis6381.conf -
在创建的文件中写入
# 继承于完整文件
include ~/myRedis/redis.conf
# 当Redis 在后台运行的时候,Redis 默认会把pid 文件放
# 在/var/run/redis.pid,你可以配置到其他地址。当运行多个
# redis 服务时,需要指定不同的pid 文件和端口
pidfile /var/run/redis_6379.pid
# 指定端口号
port 6379
# 指定rdb文件名
dbfilename dump6379.rdb -
启动三个redis服务
-
配从不配主
在从服务器上输入 slaveof <ip><port>
-
当从服务器挂掉后,重新启动后并不会成为主服务器的从服务器,需要重新输入slave命令,此时会从主服务器复制主服务器的数据
-
当主服务器挂掉后,重启后仍为原有的从服务器的主服务器
复制原理
哨兵模式
-
在自定义的/myRedis目录下新建sentinel.conf文件,名字必须是这个
-
在内容中填入
sentinel monitor mymaster 192.168.52.128 6379 1
1
代表的意思是至少有1个哨兵认为主机挂掉了,就同意切换 -
通过redis-sentinel命令启动哨兵服务
-
当关闭master服务器时,哨兵会选择一个slave(配置文件 replica-priority 最高的,如果是一样的值,则会根据pid来选取)作为master,即使master重启,其也无法再次成为原有slave的master,而是成为新的master的slave
缺点 :当master shutdown之后哨兵并不会马上发现master down了,而是有一定的延迟
Redis集群
容量不够,redis 如何进行扩容?,
并发写操作,redis如何分摊?
另外,主从模式,薪火相传模式,主机宕机,导致ip 地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。
之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。
无中心化集群的方式解决了中心化集群中服务器过多的问题
集群构建
- 需要在conf文件中加入
# 开启集群 cluster-enabled yes # 配置文件名字 cluster-config-file nodes-6379.conf # 超时时间 cluster-node-timeout 15000
-
将六个节点合成一个集群
在redis安装目录下的src文件夹
调用命令redis-cli --cluster create --cluster-replicas 1 `6个节点`(ip:point)
其中replicas 1代表以最简单的方式配置集群(一个主机一个从机)
-
集群测试
通过命令
redis-cli -c -h xx.xx.xx.xx -p xxxx
xx部分为集群中任一ip地址和端口号通过命令
cluster nodes可以查看当前集群下的所有节点
什么是slot
在创建集群的过程中我们看到,redis为master分配了插槽的位置
当我们执行set k1 value1时
使用公式CRC16(key)%16384来计算key属于哪一个插槽
故障恢复
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage为yes,那么,整个集群都挂掉。
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage为no,那么,该插槽数据全都不能使用,也无法存储。,
redis.conf中的参数cluster-require-full-coverage
jedis调用Redis集群
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
public class RedisClusterDemo {
public static void main(String[] args) {
HostAndPort hs = new HostAndPort("192.168.52.128", 6379);
JedisCluster jedisCluster = new JedisCluster(hs);
jedisCluster.set("b1","value1");
String b1 = jedisCluster.get("b1");
System.out.println(b1);
jedisCluster.close();
}
}
Redis应用问题解决
缓存穿透
- 应用服务器压力变大了
- redis命中率降低
- 一直查数据库
造成原因
- redis查询不到数据
- 出现很多非正常的url访问
解决办法
( 1)对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果( null )进行缓存,设置空结果的过期时问会很短,最长不超过五分钟。
(2)设置可访问的名单(白名单) :
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行栏截,不允许访问。
(3)采用布隆过滤器∶(布降过滤器(Bloom Filter )是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。(对第二种方法的改进,实际上也是一个bitmaps)
(4)进行实时监控∶当发现 Redis 的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
缓存击穿
现象:
- 数据库访问压力瞬时击穿
- redis里面没有出现大量的key过期
- redis正常运行
造成原因
1.redis某个key过期了,大量访问使用这个key
解决方案
key 可能会在某些时问点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。
解决问题:,
(1)预先设置热门数据∶在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热数据 key的时长
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长
(3)使用锁:。
(1)就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db。
(2)先使用缓存工具的某些带成功操作返回值的操作(比如Redis 的SETNX )去set一个mutex key
(3)当操作返回成功时,再讲行 load db 的操作,并回设缓仔,最后删除 mutex
缓存雪崩
现象:
- 数据库压力变大服务器崩溃
造成原因
- 在极少时间短,查询大量key的集中过期情况
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕!解决方案∶,
(1)构建多级缓存架构:
nginx缓存+redis缓存+其他缓存(ehcache等 )
(2)使用锁或队列:
用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况·
(3)设置过期标志更新缓存:·
记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
(4)将缓存失效时间分散开∶.
比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
分布式锁
随着业务发展的需要,原单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
基于redis实现分布式锁
使用setnx命令存放数据,setnx判定redis中是否有该key,如果没有才能正确set,否则返回失败
使用setnx上锁,通过del释放锁
锁一直没有释放,设置key过期时间,自动释放(expire key time)
可以使用
set [key] [value] nx ex [outtime]
一句话为key上锁和设置过期时间
在java中实现分布式锁
在springboot中引入依赖后
public void testLock(){
//设置锁,并将锁的过期时间设置为3秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3, TimeUnit.SECONDS);
if(lock){
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)){
return;
}
//有值转成int
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",++num);
redisTemplate.delete("lock");
}else{
//获取锁失败,每隔0.1秒再获取
try{
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
锁的误删
![[…/image/
]
存在b的锁被a释放了
解决方法
- uuid表示不同的操作
set lock uuid nx ex 10 - 释放锁时候,首先判断当前uuid和要释放锁uuid是否一样
public void testLock(){
String uuid = UUID.randomUUID().toString();
//设置锁,并将锁的过期时间设置为3秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
if(lock){
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)){
return;
}
//有值转成int
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",++num);
//判断uuid值是否一样
String lockUUID = (String)redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUUID)){
redisTemplate.delete("lock");
}
}else{
//获取锁失败,每隔0.1秒再获取
try{
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Lua保证删除原子性
误删情况:
当a判断完毕uuid一致,想要进行删除操作的时候,还没有删除,锁到了固定时间,释放了锁,此时b获取了锁,然而此时a执行delete释放了锁,此时释放的是b的锁,出现问题
解决方法:
使用lua脚本保证删除的原子性
@GetMapping("testLockLua")
public void testLockLua(){
String uuid = UUID.randomUUID().toString();
//设置锁,并将锁的过期时间设置为3秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS);
if(lock){
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)){
return;
}
//有值转成int
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",++num);
//判断uuid值是否一样
String lockUUID = (String)redisTemplate.opsForValue().get("lock");
String scrip = "if redis.call('get',KEYS[1]) == ARGC[1] then " +
"return redis.call('del',KEYS[1])"+
"else" +
"return 0"+
"end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(scrip);
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
}else{
//获取锁失败,每隔0.1秒再获取
try{
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
分布式锁可用条件
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:。
-互斥性。在任意时刻,只有一个客户端能持有锁。
-不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
-解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
-加锁和解锁必须具有原子性。