1、NoSQL非关系型的数据库
1.不依赖业务逻辑数据库存储,以简单key-value存储,数据库的扩展能力强
2.不遵循SQL标准
3.不支持ACID
适用于
不适用于
2、NoSQL优点
1.缓存数据库,完全在内存中,速度快,数据结构简单
2.减少io操作,数据库和表拆分,虽然破坏业务逻辑,即外加一个缓存数据库,提高数据库速度,也可以用专门的存储方式,以及针对不同的数据结构存储
3、其他数据库
1、特点
2、应用场景
高速缓存
持久化数据
3、安装
1、安装gcc:C语言编译环境
yum install gcc
gcc -version
2、安装redis
安装包目录:cd /usr/local/src
下载:wget https://download.redis.io/releases/redis-6.2.1.tar.gz
解压:tar -zxvf redis-6.2.1.tar.gz
进入目录:cd redis-6.2.1
编译:make
安装:make install
默认位置:cd /usr/local/bin 安装完成
redis-benchmark:性能测试工具
redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲
redis-check-dump:修复有问题的dump.rdb文件
redis-sentinel:Redis集群使用
redis-server:Redis服务器启动命令
redis-cli:客户端,操作入口
3、后台启动
备份redis.conf:
cd /usr/local/src/redis-6.2.1
cp redis.conf /etc/redis.conf
修改配置文件:
cd /ect
vi redis.conf
修改:daemonize yes
修改密码:requirepass xxxxxx【重要】
启动
cd /usr/local/bin
redis-server /etc/redis.conf
客户端连接
redis-cli -a xxxxxx【密码】
查看设置的密码:config get requirepass
配置文件
修改redis配置文件,表示支持远程访问
注释:# bind 127.0.0.1
修改protected-mode no
4、关闭
查看redis的端口号:ps -ef | grep redis
kill -9 xxx(端口号)
redis-clli shutdown
5、连接redis可能遇到的问题
关闭linux防火墙(坑)
需要打开防火墙,开放对应的端口 6379
systemctl start firewalld # 开启防火墙
systemctl status firewalld # 查看防火墙的状态
firewall-cmd --zone=public --add-port=6379/tcp --permanent # 开放端口 6379
firewall-cmd --reload # 重新加载对应的防火墙策略
4、相关知识点
Redis是单线程+多路IO复用技术
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数
1、 key值键位
key值的操作:
keys *
查看当前库所有key
set key value
设置key值与value
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
通杀全部库2、string字符串
参数设置:
set key value
设置key值get key
查询key值append key value
将给定的value追加到原值末尾strlen key
获取值的长度setnx key value
只有在key不存在的时候,设置key值incr key
将key值存储的数字增1,只对数字值操作,如果为空,新增值为1decr key
将key值存储的数字减1,只对数字值操作,如果为空,新增值为1incrby/decrby key <步长>
将key值存储的数字增减如步长补充:
补充额外的字符串参数:
mset key value key value..
同时设置一个或者多个key-valuemget key key...
同时获取一个或多个valuemsetnx key value key value..
同时设置一个或者多个key-value.当且仅当所有给定key都不存在getrange key <起始位置> <结束位置>
获取key的起始位置和结束位置的值setrange key <起始位置>
value 将value的值覆盖起始位置开始setex key <> value
设置键值的同时,设置过期时间getset key value
用新值换旧值3、list列表
常用命令:
lpush/rpush key value value...
从左或者右插入一个或者多个值(头插与尾插)lpop/rpop key
从左或者右吐出一个或者多个值(值在键在,值都没,键都没)rpoplpush key1 key2
从key1列表右边吐出一个值,插入到key2的左边lrange key start stop
按照索引下标获取元素(从左到右)lrange key 0 -1
获取所有值lindex key index
按照索引下标获得元素llen key
获取列表长度linsert key before/after value newvalue
在value的前面插入一个新值lrem key n value
从左边删除n个value值lset key index value
在列表key中的下标index中修改值value4、set集合
字典,哈希表
自动排重且为无序的
常用命令:
sadd key value value...
将一个或者多个member元素加入集合key中,已经存在的member元素被忽略smembers key
取出该集合的所有值sismember key value
判断该集合key是否含有改值scard key
返回该集合的元素个数srem key value value
删除集合中的某个元素spop key
随机从集合中取出一个元素srandmember key n
随即从该集合中取出n个值,不会从集合中删除smove <一个集合a><一个集合b> value
将一个集合a的某个value移动到另一个集合bsinter key1 key2
返回两个集合的交集元素sunion key1 key2
返回两个集合的并集元素sdiff key1 key2
返回两个集合的差集元素(key1有的,key2没有)5、hash哈希
键值对集合,特别适合用于存储对象类型
常用命令:
hset key field value
给key集合中的filed键赋值valuehget key1 field
集合field取出valuehmset key1 field1 value1 field2 value2
批量设置hash的值hexists key1 field
查看哈希表key中,给定域field是否存在hkeys key
列出该hash集合的所有fieldhvals key
列出该hash集合的所有valuehincrby key field increment
为哈希表key中的域field的值加上增量1 -1hsetnx key field value
将哈希表key中的域field的值设置为value,当且仅当域field不存在6、Zset有序集合
没有重复元素的字符串集合,按照相关的分数进行排名,排名从低到高,排名可重复
有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合
常用命令:
zadd key score1 value1 score2 value2
将一个或多个member元素及其score值加入到有序key中zrange key start stop (withscores)
返回有序集key,下标在start与stop之间的元素,带withscores,可以让分数一起和值返回到结果集。zrangebyscore key min max(withscores)
返回有序集key,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score的值递增次序排列zrevrangebyscore key max min (withscores)
同上,改为从大到小排列zincrby key increment value
为元素的score加上增量zrem key value
删除该集合下,指定值的元素zcount key min max
统计该集合,分数区间内的元素个数zrank key value
返回该值在集合中的排名,从0开始SELECT
命令在连接上指定数据库id发送者 (pub) 发送消息,订阅者 (sub) 接收消息
Redis 客户端可以订阅任意数量的频道
1、 打开一个客户端订阅channel1:SUBSCRIBE channel1
2、打开另一个客户端,给channel1发布消息hello:publish channel1 hello
3、打开第一个客户端可以看到发送的消息
1、Bitmaps
命令参数:
setbit key offset value
设置值
getbit key offset
获取某个偏移量的值
bitcount
统计数值
bitcount key (start end)
统计字符串从start 到end字节比特值为1的数量
redis的setbit设置或清除的是bit位置,而bitcount计算的是byte的位
bitop and(or/not/xor)destkey key
复合操作,交并非异或,结果保存在destkey
2、HyperLogLog
命令参数:
pfadd key element
添加指定的元素到hyperloglog中,成功则返回1,不成功返回0pfcount key
计算key的近似基数,即添加了多少个不重复元素pfmerge destkey sourcekey sourcekey
一个或多个key合并后的结果存在另一个key3、Geographic
提供经纬度设置,查询范围,距离查询
命令参数:
geoadd key longitude latitude member
添加地理位置(经度纬度名称)geopos key member
获取指定地区的坐标值geodist key member1 member2 (m km ft mi)
获取两个位置之间的直线距离georadius key longitude latitude radius (m km ft mi)
以给定的经纬度为中心,找出某一半径的内元素配置
<dependencies>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.6.1version>
dependency>
dependencies>
测试常用操作命令
@Test
public void fun() {
Jedis jedis = new Jedis("192.168.55.129", 6379);
jedis.auth("xxxxxx"); // 设置密码
jedis.set("k1", "v1");
String k1 = jedis.get("k1");
System.out.println(k1);
Set<String> keys = jedis.keys("*");
for (String key : keys) {
System.out.println(key);
}
}
jedis.close();
注意:连接失败检查
关闭linux防火墙:systemctl stop firewalld.service
redis.conf中注释掉bind 127.0.0.1 ,然后 protected-mode no
案例:手机验证码功能
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
Random随机生成6位数;设置过期时间120s
2、输入验证码,点击验证,返回成功或失败
比较redis中的验证码和输入的验证码
3、每个手机号每天只能输入3次
incr每次发送之后+1,大于2时提示不能发送
package com.mys.jedis;
import redis.clients.jedis.Jedis;
import java.util.Random;
/**
* @author mys
* @date 2022/11/13 16:18
*/
public class PhoneCode {
public static void main(String[] args) {
// 模拟验证码发送
verifyCode("13811111111");
// getRedisCode("13811111111", "614493");
}
// 1. 生成6位随机数
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;
}
// 2.每个手机每天只能发送三次,验证码放到redis中并设置过期时间
public static void verifyCode(String phone) {
// 连接redis
Jedis jedis = new Jedis("192.168.55.129", 6379);
jedis.auth("xiaoqian");
// 手机发送次数key
String countKey = "VerifyCode" + phone + ":count";
// 验证码key
String codeKey = "VerifyCode" + phone + ":code";
// 每个手机每天只能发送3次
String count = jedis.get(countKey);
if (count == null) {
jedis.setex(codeKey, 24*60*60, "1");
} else if(Integer.parseInt(count) <= 2) {
jedis.incr(countKey);
} else {
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.55.129", 6379);
jedis.auth("xiaoqian");
// 验证码key
String codeKey = "VerifyCode" + phone + ":code";
String redisCode = jedis.get(codeKey);
if (redisCode.equals(code)) {
System.out.println("success");
} else {
System.out.println("fail");
}
jedis.close();
}
}
1、整合依赖文件
springboot和连接池
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.6.0version>
dependency>
2、application.properties配置redis配置
#Redis服务器地址
spring.redis.host=192.168.55.129
spring.redis.password=xiaoqian
#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
3、添加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;
}
}
4、测试类
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
redisTemplate.opsForValue().set("name", "mys");
String name = (String) redisTemplate.opsForValue().get("name");
return name;
}
}
1、事务
作用:串联多个命令防止别的命令插队
命令:
组队中某个命令出现了错误,执行时整个的所有队列都会被取消
执行中某个命令出现了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚
悲观锁:每次操作都上锁
乐观锁:即给数据加上版本号,同步更新数据以及加上版本号
watch:在执行multi之前,先执行watch key1 [key2]
,可以监视key ,如果在事务执行之前,key 被其他命令所改动,那么事务将被打断
unwatch:取消 WATCH 命令对所有 key 的监视
事务的三特性
对比一下sql的事务特性,为acid,一致性,隔离性,原子性,持久性
而redis的事务特性没有隔离,因为都是单独的隔离而不会被打断,而且也不保证原子性,错了等待不到消息不会进行回滚
2、事务的秒杀案例
在redis存入商品数,设定秒杀时间,提供用户秒杀窗口,用户秒杀成功,redis中商品数-1,用户信息也存入redis中(为了相同用户只能秒杀一次)
基本实现
public class SecKill {
public static boolean doSecKill(String uid, String prodid) throws Exception{
// 1.uid和prodid非空判断
if (uid == null || prodid == null) {
return false;
}
// 2.连接redis
Jedis jedis = new Jedis("192.168.55.129", 6379);
jedis.auth("xiaoqian");
// 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("已经秒杀成功,不能重复秒杀");
}
// 6.判断商品数量,如果库存小于1,秒杀结束
if (Integer.parseInt(kc) <= 0) {
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
// 7.秒杀过程
// 7.1 库存-1
jedis.decr(kcKey);
// 7.2 把秒杀成功用户添加到清单里面
jedis.sadd(userKey, uid);
System.out.println("秒杀成功了");
jedis.close();
return true;
}
}
高并发测试
下载工具:yum install httpd-tools
-n:请求次数
-c:并发次数
-T:设计的类型,可以是post,get
-p:提交的参数
ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://172.22.xxx:8081/Seckill/doseckill
问题:超卖;连接超时
1、通过连接池,解决超时问题
// 通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
2、通过乐观锁,解决超卖问题
// (1)监视库存
jedis.watch(kcKey);
// 4.获取库存,如果库存null,秒杀还没开始
// (2)使用事务
Transaction multi = jedis.multi();
// 组队
multi.decr(kcKey);
multi.sadd(userKey, uid);
// 执行
List<Object> res = multi.exec();
if (res == null || res.size() == 0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
// 7.秒杀过程
3、库存遗留问题
乐观锁造成库存遗留问题
解决:引入lua脚本
通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题
1、RDB
在指定的时间间隔内
将内存中的数据集快照
写入磁盘
备份流程:
Fork:复制一个与当前进程一样的进程,并作为原进程的子进程,一般情况父进程和子进程会共用同一段物理内存
数据如果有变化的,会在/usr/local/bin
目录下生成一个dump.rdb的文件
配置文件:
备份与恢复:
优点:
缺点:
停止RDB:redis-cli config set save ""
2、AOF
以日志的形式来记录每个写操作,只许追加文件但不可以改写文件
持久化流程:
特点:
AOF默认不开启
AOF和RDB同时开启,默认读取AOF数据
备份恢复流程和RDB一样,都是拷贝文件,恢复时再使用
正常恢复:修改默认的appendonly no,改为yes;拷贝aof文件,重启redis
异常恢复:如果遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof
进行恢复
同步频率:
master/slaver机制,Master以写为主,Slave以读为主
搭建一主两从流程:
创建/myredis文件夹
复制redis.conf配置文件
一主两从,创建三个配置文件
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
启动三个服务
redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf
redis-cli -p 6379 -a xxx
redis-cli -p 6380 -a xxx
redis-cli -p 6381-a xxx
查看当前主机的运行状态:info replication
在从机上执行slaveof 主机ip 端口号
命令:
info replication
查看信息
slaveof IP 端口
9可以设置从服务器
redis-server redis.conf
启动服务器端
redis-cli -p 端口
启动客户端
ps -ef | grep redis
:查看redis进程
特点:
原理:
薪火相传
主——> 从1——>从2
反客为主
slaveof no one
从机变成主机
非自动
哨兵模式
反客为主的自动版
主要是为了监控主机宕机之后,从机可以立马变为主机,就和上面的反客为主一样,但不用手动设置。能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
流程:
/myredis/sentinel.conf
sentinel monitor mymaster 127.0.0.1 6379 1
redis-sentinel /myredis/sentinel.conf
选择从服务器优先级:
slave-priority 100,值越小优先级越高
选择偏移量最大的
选择runid最小的从服务
1、问题:
2、方法:
代理主机
无中心化集群(推荐)
任何一台服务器都可以作为集群入口,并且之间互相连通
3、集群:
4、搭建集群流程:
删除rdb aof文件
创建6个实例 6379 6380 6381 6389 6390 6391
分别修改配置文件
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
启动6个服务
将6个节点合成一个集群
需要进入到/usr/local/src/redis-6.2.1/src
目录下执行
redis-cli --cluster create --cluster-replicas 1 192.168.55.129:6379 192.168.55.129:6380 192.168.55.129:6381 192.168.55.129:6389 192.168.55.129:6390 192.168.55.129:6391 -a xxx
redis-cli -c -p 6379
用集群的方式连接
cluster nodes
查看集群信息
节点分配原则:尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上
5、slots:插槽
一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个
集群使用公式 CRC16(key) % 16384
来计算键 key 属于哪个槽
不能使用mget,mset等多键操作,通过{}来定义组的概念
mset k1 v1 k2 v2
=>×
mset k1{user} v1 k2{user} v2
=> √
查询集群中的值 cluster keyslot k1
cluster countkeysinslot 4478
cluster get keysinslot 4478 10
6、故障恢复
7、优点:
8、缺点:
1、缓存穿透
特点:
原因:redis查不到数据,并且出现很多非正常url访问
解决方案:
2、缓存击穿
特点:
原因:redis中某个key过期,大量访问使用这个key
解决方案:
3、缓存雪崩
特点:数据库压力变大,服务器崩溃
原因:在极少时间段,查询大量key集中过期
解决方案:
4、分布式锁
实现方案:
使用Redis实现分布式锁:
setnx
设置锁,del
释放锁
exprie
设置key的过期时间,自动释放
上锁之后,突然出现异常,无法设置过期时间
set users 10 nx ex 12
既上锁又设置过期时间UUID防误删
@GetMapping("testLock")
public void testLock(){
//1获取锁,setne ,顺便设置过期时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS);
//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();
}
}
}
UUID防误删:
多一个判断是否是你的锁,虽然是共享锁,都是一样的,但是可以上锁之后在设置时间,还要给每个用户的这把锁都来一个uuid
@GetMapping("testLock")
public void testLock(){
String uuid = UUID.randomUUID().toString();
//1获取锁,setne ,顺便设置过期时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS);
//2获取锁成功、查询num的值
if(lock){
...
String lockUuid = (String)redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUuid)){
//2.4释放锁,del
redisTemplate.delete("lock");
}
}else{
...
}
}
lua脚本保证原子性