NoSQL是指非关系型数据库(Not Only SQL),是一种用于存储和检索非结构化和半结构化数据的数据库管理系统。与传统的关系型数据库不同,NoSQL数据库通常使用非结构化数据模型,例如键值存储、文档存储、列存储或图形数据库等。
NoSQL数据库通常更具扩展性、可用性和灵活性,并支持高可扩展性和高性能。它们通常在处理大数据和高负载应用程序时表现出色。许多大型企业和互联网公司,如亚马逊、谷歌和Facebook等,都在使用NoSQL数据库来处理海量数据。
键值存储数据库(Key-Value Store):使用哈希表来存储键值对,类似于字典或者散列表,它们可以快速地进行插入、查找和删除操作。常见的键值存储数据库包括Redis、Memcached和BerkeleyDB等。
文档数据库(Document Database):使用类似于JSON或XML格式的文档来存储数据,可以将多个文档组织成集合或者文档库。文档数据库支持更为复杂的数据结构,比如嵌套的文档和数组。常见的文档数据库包括MongoDB和CouchDB等。
列族存储数据库(Column-Family Store):将数据存储在列族中,每个列族包含多个列,每个列又可以存储多个值,可以看作是一个多维数组,支持大规模的数据存储和高效的查询操作。常见的列族存储数据库包括HBase和Cassandra等。
图形数据库(Graph Database):使用图形来表示数据,节点表示实体,边表示实体之间的关系,可以用于处理复杂的关系型数据,比如社交网络和知识图谱等。常见的图形数据库包括Neo4j和OrientDB等。
Redis(Remote Dictionary Server ),即远程字典服务。
是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
下载地址:https://github.com/microsoftarchive/redis/releases/tag/win-3.2.100
首先我们从redis英文官网下下载最新版本的redis压缩包
官网地址
然后我们把解压包放到服务器上的/opt目录下
然后我们进行一下解压
tar -zxvf redis-7.0.10.tar.gz
进入redis文件
cd redis-7.0.10/
安装gcc环境
yum install gcc-c++
由于我本人的服务器之前安装docker的时候已经安装过了,所以这里显示我不需要重复安装
然后在当前目录下安装
make
再次确认安装完成
make install
redis的默认安装路径在usr/local/bin
然后我们将redis的配置文件拷贝到当前目录下
首先我们先创建一个config目录
mkdir config
然后我们复制配置文件到config目录下
cp /opt/redis-7.0.10/redis.conf config
然后我们需要修改一下redis的配置文件,因为redis默认配置不是后台启动的所以我们这里需要修改一下
vim /usr/local/bin/config/redis.conf
改成yes
然后我们就可以根据指定的配置文件来启动服务啦啦啦啦啦啦啦
cd /usr/local/bin
redis-server config/redis.conf
然后我们通过redis.cli进行测试一下是否服务启动成功
通过ping pong你就可以知道了
恭喜你,可以愉快的玩耍了
不过同样的我们也可以在外部通过
ps -ef|grep redis
来查看redis进程是否已经启动
然后我们来进行一下退出的操作
shutdown
exit
从redis.conf文件我们可以看到redis默认的数据库是有16个的,默认使用第0个数据库
select 3
DBSIZE
set name imperfect
DBSIZE
flushdb
DBSIZE
FLUSHALL
SELECT 3
DBSIZE
SELECT 1
DBSIZE
set key value : 新增或更新字符串键值对
mset key value [key1 value1 …]:批量新增或更新键值对
setnx key value :如果key不存在就添加,否则就失败
setex key seconds value:设置简直对的时同时设置过期时间
get key :获取指定key的值
mget key [key1 key2 …]:获取多个key的值
del key [key1 key2 …]:删除指定key
expire key seconds:设置指定key过期时间,以秒为单位
ttl key:查看指定key还剩余多长时间
incr key:将指定key存储的数值加1
decr key:将指定key存储的数值减1
incrby key step:将指定key存储的数值加上step
decrby key step :将指定key存储的数值减去step
lpush key value [value1 …] :在指定key的列表左边插入一个或多个值;
rpush key value [value1 …] :在指定key的列表右边插入一个或多个值;
lpop key :从指定key的列表左边取出第一个值;
rpop key:从指定key的列表右边取出第一个值;
lrange key start end:从指定key列表中获取指定区间内的数据;
blpop key [key1 …] timeout:从指定key列表中左边取出第一个值,若列表中没有元素就等待timeout时间,如果timeout为0就一直等待。
brpop key [key1 …] timeout:从指定key列表中右边取出第一个值,若列表中没有元素就等待timeout时间,如果timeout为0就一直等待。
lset key index value:将指定下标的值更新为value,
set是一种无序不重复集合。与List类似,所有的set命令都是以s开头的
SADD key member1 [member2] 向集合添加一个或多个成员
SCARD key 获取集合的成员数
SDIFF key1 [key2] 返回给定所有集合的差集
SDIFFSTORE destination key1 [key2] 返回给定所有集合的差集并存储在 destination 中
SINTER key1 [key2] 返回给定所有集合的交集
SINTERSTORE destination key1 [key2] 返回给定所有集合的交集并存储在 destination 中
SISMEMBER key member 判断 member 元素是否是集合 key 的成员
SMEMBERS key 返回集合中的所有成员
SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合
SPOP key 移除并返回集合中的一个随机元素
SRANDMEMBER key [count] 返回集合中一个或多个随机数
SREM key member1 [member2] 移除集合中一个或多个成员
SUNION key1 [key2] 返回所有给定集合的并集
SUNIONSTORE destination key1 [key2] 所有给定集合的并集存储在 destination 集合中
SSCAN key cursor [MATCH pattern] [COUNT count] 迭代集合中的元素
HDEL key field2 [field2] 删除一个或多个哈希表字段
HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。
HGET key field 获取存储在哈希表中指定字段的值/td>
HGETALL key 获取在哈希表中指定 key 的所有字段和值
HINCRBY key field increment 为哈希表 key 中的指定字段的整数值加上增量 increment 。
HINCRBYFLOAT key field increment 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
HKEYS key 获取所有哈希表中的字段
HLEN key 获取哈希表中字段的数量
HMGET key field1 [field2] 获取所有给定字段的值
HMSET key field1 value1 [field2 value2 ] 同时将多个 field-value (域-值)对设置到哈希表 key 中。
HSET key field value 将哈希表 key 中的字段 field 的值设为 value 。
HSETNX key field value 只有在字段 field 不存在时,设置哈希表字段的值。
HVALS key 获取哈希表中所有值
HSCAN key cursor [MATCH pattern] [COUNT count] 迭代哈希表中的键值对。
有序集合,在set的基础上增加了一个值(底层是用跳表实现的),与之前的类似,所有的Zset命令以Zset开头
ZADD [key] [n] [value] 向key中添加value值,排序大小为n
n必须是数值
ZRANGE [key] [start] [end] 查询key中以start开始,end结束的所有元素,升序排序
start和end取闭区间
支持负数逆向
ZREVRANGE [key] [start] [end] 查询key中以start开始,end结束的所有元素,倒序排序
start和end取闭区间
支持负数逆向
ZRANGEBYSCORE [key] [min] [max] [WITHSCORES?] 将指定key在min到max范围内升序排序
如果min>max,结果只会是空的
WITHSCORES是可选项,如果写了该选项,则会将分数显示在key后面
+inf代表无穷大 -inf代表无穷小
ZREVRANGEBYSCORE [key] [min] [max] [WITHSCORES?] 将指定key在min到max范围内降序排序
ZREM [key] [item] 移除key集合中的item元素
ZCARD [key] 获取有序集合中的元素个数
ZCOUNT [key] [min] [max] 获取指定区间的成员数量
Redis GEOHASH 命令 返回一个或多个位置元素的 Geohash 表示
Redis GEOPOS 命令 从key里返回所有给定位置元素的位置(经度和纬度)
Redis GEODIST 命令 返回两个给定位置之间的距离
Redis GEORADIUS 命令 以给定的经纬度为中心, 找出某一半径内的元素
Redis GEOADD 命令 将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
Redis GEORADIUSBYMEMBER 命令 找出位于指定范围内的元素,中心点是由给定的位置元素决定
什么是基数?
例如下面这个例子
A{1,3,5,6,7}
这里A的基数就是5
B{1,3,4,4,5}
这里B的基数就是4
在Redis2.8.9版本就更新了Hyperloglog数据结构了。
使用场景:通常用于网页的UV(一个人访问一个网站多次,但是还是算作一个人!)
常用命令:
Redis Pgmerge 命令 将多个 HyperLogLog 合并为一个 HyperLogLog
Redis Pfadd 命令 添加指定元素到 HyperLogLog 中。
Redis Pfcount 命令 返回给定 HyperLogLog 的基数估算值。
什么是Bitmaps?
我们都知道bit,是计算机当中最小的存储单位,8个bit组成一个Byte,而bit只能存储两个值,0和1。
BitMap可以理解为存储bit的数组,多个bit存储后组成的一个特定结构,每个位置只能存储1和0。
使用场景:统计用户信息,活跃不活跃,登录未登录,365天是否打卡,只要是两个状态的都可以使用Bitmaps!
常用命令:
Redis Getbit 命令 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。
Redis Setbit 命令 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
Redis单条命令保证原子性,但是事务不保证原子性!
Redis事务本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序来执行。
Redis事务没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会去执行!
这里我先介绍一下redis关于事务的几个命令:
Redis Exec 命令 执行所有事务块内的命令。
Redis Watch 命令 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
Redis Discard 命令 取消事务,放弃执行事务块内的所有命令。
Redis Unwatch 命令 取消 WATCH 命令对所有 key 的监视。
Redis Multi 命令 标记一个事务块的开始。
1、开启事务(Multi)
2、命令入队(…)
3、执行事务(Exec)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) "v2"
4) OK
由以上可以看出入队的时候命令并没有被执行,只有真正进行执行语句的时候,这些命令才在队列中依次执行。(为什么说Redis事务并没有隔离性的概念呢,就以上的例子,你可以参考回我之前写过关于mysql事务中的特点,mysql存在事务隔离级别,就算在一个事务中执行SQL,他也是会根据事务隔离级别去进行隔离的。但是这里redis事务当中并没有在编写命令的时候就执行,而是到最后的时候才按照队列进行执行)
1、开启事务(Multi)
2、命令入队(…)
3、放弃事务(Discard)
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> Discard
OK
127.0.0.1:6379> get k4
(nil)
从这个例子中我们可以看到,这里由于我们抛弃了这个事务,所以get k4的时候是获取不到的。
如果命令有编译的问题,事务中所有的命令都不会被执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
(nil)
从以上的例子中我们可以看到,这个getset k3命令在检查的时候已经有错误了,导致整个事务中所有的命令都不会被执行。
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR value is not an integer or out of range
2) OK
127.0.0.1:6379> get k2
"v2"
在这个例子中对k1这个字符串进行加1是不能够加出来的,但是incr k1这个语法并没有错误,这里的命令会直到执行的时候才会发生报错,但是整个事务当中并没有影响到其他的命令。所以redis是不会保证整个事务中的原子性。
很悲观,认为什么时候都会出现问题,无论做什么都会加锁!
很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间有没有人去修改过这个数据。
Redis Watch 命令 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
Redis Unwatch 命令 取消 WATCH 命令对所有 key 的监视。
首先我们可以使用两个xshell的客户端来模拟这种场景
首先我们搭建环境
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> set out 0
OK
时间点 | 事务A | 事务B |
---|---|---|
1 | watch money | |
2 | multi | |
3 | decrby money 100 | |
4 | incrby out 100 | multi |
5 | incrby money 100 | |
6 | exec | |
7 | exec |
事务A执行结果
事务A提交失败,当开启了监视之后,在事务执行之前,会先检查数据是否被修改了。
创建java项目编写代码通过jedsi连接redis
首先我们来部署一下环境
导入一下这两个maven依赖
<dependencies>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>5.0.0-alpha1version>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2artifactId>
<version>2.0.26version>
dependency>
dependencies>
编写测试代码
package com.imperfect;
import com.alibaba.fastjson2.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* @author : Imperfect(lxm)
* @Des:
* @date : 2023/3/31 16:47
*/
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis(目标服务器, 6379);
jedis.auth(密码);
System.out.println(jedis.ping());
}
}
这里redis更加多的命令在jedis的包当中也有详细接口文档,我就不再一一叙述了
package com.imperfect;
import com.alibaba.fastjson2.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* @author : Imperfect(lxm)
* @Des:
* @date : 2023/3/31 16:47
*/
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis(目标服务器, 6379);
jedis.auth(验证密码);
System.out.println(jedis.ping());
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "sam");
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
//开启事务
try {
multi.set("user1", result);
multi.set("user2", result);
multi.exec();
} catch (Exception e) {
multi.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close();
}
}
}
package com.imperfect;
import com.alibaba.fastjson2.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
/**
* @author : Imperfect(lxm)
* @Des:
* @date : 2023/3/31 16:47
*/
public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis(目标服务器, 6379);
jedis.auth(验证密码);
System.out.println(jedis.ping());
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "sam");
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
//开启事务
try {
multi.set("user3", result);
multi.set("user4", result);
int i=0/0;
multi.exec();
} catch (Exception e) {
multi.discard();
e.printStackTrace();
} finally {
System.out.println(jedis.get("user3"));
System.out.println(jedis.get("user4"));
jedis.close();
}
}
}
这里通过执行结果我们可以看出,这里遇到了异常后,我们通过discard回滚了事务。
首先我们配置一下所需要的环境,IDEA中新建一个SpringBoot项目,并且导入相对应所需要的jar包
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.5version>
<relativePath/>
parent>
<groupId>com.imperfectgroupId>
<artifactId>redis-02-springbootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>redis-02-springbootname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
编写application.properties文件
src/main/resources/application.properties
spring.redis.host=主机地址
spring.redis.port=端口
spring.redis.password=密码
编写SpringBoot测试类
package com.imperfect;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
@SpringBootTest
class Redis02SpringbootApplicationTests {
@Resource
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("mykey","imperfect");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
测试结果如下:
这样我们就成功集成到SpringBoot当中啦啦啦啦啦啦啦 ♂️
首先我们先创建一下所需要用到的实体类
package com.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* @author : Imperfect(lxm)
* @Des:
* @date : 2023/4/4 14:35
*/
@Component
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String name;
private Integer age;
}
直接编写测试代码
@Test
void test() throws JsonProcessingException {
User user = new User("sam", 3);
redisTemplate.opsForValue().set("user",user);
System.out.println(redisTemplate.opsForValue().get("user"));
}
我们可以看源码RedisTemplate当中
默认是使用JDK进行序列化的
而这里的报错是 Failed to serialize object using DefaultSerializer
所以这里我们需要自定义RedisTemplate进行序列化
src/main/java/com/config/RedisConfig.java
package com.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author : Imperfect(lxm)
* @Des:
* @date : 2023/4/4 14:27
*/
@Configuration
public class RedisConfig {
//自己定义的一个RedisTemplate
@Bean
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;
}
}
再进行测试后发现成功插入
通常在企业当中我们会使用RedisUtil工具类来进行操作哦,就像下面这样子
package com.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
这里我们直接打开redis.conf
配置 | 解释 |
---|---|
bind 127.0.0.1 | 绑定的ip |
protected-mode yes | 保护模式 |
port | 端口设置 |
daemonize yes | 以守护进程的方式运行,默认为no,即后台运行 |
pidfile /var/run/redis_6379.pid | 如果以后台的方式运行,我们就需要指定一个pid文件 |
loglevel notice | 日志打印级别 |
logfile | 日志的文件位置名 |
database 16 | 数据库的数量 |
always-show-logo yes | 是否显示logo |
save 3600 1 | 如果3600s内,如果至少有1个key进行了修改,我们就会进行持久化操作 |
save 300 10 | 如果300s内,如果至少有10个key进行了修改,我们就会进行持久化操作 |
save 60 10000 | 如果60s内,如果至少有10000个key进行了修改,我们就会进行持久化操作 |
stop-writes-on-bgsave-error yes | 持久化出错了,是否继续进行工作 |
rdbcompression yes | 是否压缩rdb文件,需要消耗一些CPU的资源 |
rdbchecksum yes | 保存rdb文件的时候,是否进行错误校验 |
dir ./ | rdb文件保存的目录 |
requirepass | 设置密码 |
maxclients 10000 | 设置客户端连接最大数 |
maxmemory | 配置redis最大内存 |
maxmemory-policy noeviction | 内存爆满的时候的执行策略:1、volatile-lru:只对设置了过期时间的key进行LRU算法进行删除2、allkeys-lru : 对所有key执行LRU算法进行删除3、volatile-lfu:只对设置了过期时间的key进行LFU算法进行删除4、allkeys-lfu:对所有key执行LFU算法进行删除5、volatile-random:随机删除设置有过期时间的key6、allkeys-random:随机删除7、volatile-ttl : 删除即将过期的8、noeviction : 永不过期,返回错误 |
appendonly no | 默认不开启AOF模式的,默认是使用RDB方式持久化的 |
appendfilename “appendonly.aof” | 持久化的文件的名字 |
RDB 是 Redis DataBase 的缩写,即内存块照。因为Redis的数据时存在内存中的,当服务器宕机时,Redis中存储的数据就会丢失。这个时候就需要内存快照来恢复Redis中的数据了。快照就是在某一时刻,将Redis中的所有数据,以文件的形式存储起来。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,它恢复时是将快照直接读到内存里。
Redis会单独创建(fork)子进程来进行持久化,会先将数据写入到一个 临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
优点:如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效
缺点:最后一次持久化后的数据可能丢失
Redis默认的持久化方式就是RDB方式
RDB方式保存的文件,默认的文件名就是dump.rdb
快照触发机制♀️
redis.conf配置的save的规则满足的情况下
执行flushall命令
执行save命令
关闭Redis时
AOF (Append Only File) 持久化默认是关闭的,通过将 redis.conf 中将 appendonly no,修改为 appendonly yes 来开启AOF 持久化功能,如果服务器开始了 AOF 持久化功能,服务器会优先使用 AOF 文件来还原数据库状态。只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态。
AOF持久化模式又叫文件追加模式,就是在Redis进行写操作的时候,将写命令追加在AOF文件的最后,相当于一个历史操作记录文件一般,该操作每秒执行一次,数据的完整度会比较高。
Redis的AOF持久化操作默认是不开启的,通过将appendonly设置成yes开启,重启Redis后生效。
RDB和AOF持久化方式同时开启的话,Redis启动或恢复数据时,优先使用AOF方式。
如果aof文件损坏,则启动Redis时会由于AOF文件损坏而启动失败,这时候可以通过redis-check-aof修复aof文件,通过命令redis-check-aof --fix修复aof文件,修复会将错误的命令行去除。
如果在开启AOF模式之后将aof文件删除,那么Redis写命令追加到aof失败,不会再创建aof文件。
AOF重写:当AOF文件达到auto-aof-rewrite-percentage及auto-aof-rewrite-min-size两个配置的要求时,会触发重写操作,重写操作实质上是Redis创建一个新的AOF文件来替代现有的AOF文件,新的AOF文件的会将可合并的命令合并,减少浪费空间的冗余命令。
优点:数据完整性好。
缺点:从文件的角度来说,AOF文件大小远远大于RDB文件,修复的速度也比RDB慢。AOF运行效率也要比RDB低。
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统等
Redis客户端可以订阅任意数量的频道
-消息发布者
-频道
-消息订阅者
下图展示了频道channel1,以及订阅这个频道的三个客户端——client2、client5和client1之间的关系:
发布者发布消息后会被订阅者接收
那接下来我们就来实战一下吧♀️
客户端A
subscribe imperfect
客户端B
publish imperfect "hello,imperfect"
publish imperfect "hello,redis"
然后我们来看一下执行的结果
客户端A
发布订阅实现原理
Redis是使用C实现的,通过分析Redis源码里面的pubsub.c文件,了解发布和订阅机制的底层实现,藉此加深对Redis的理解
Redis通过PUBLISH、SUBSCRIBE和PSUBSCRIBE等命令实现发布和订阅功能
通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键,就是将客户端添加到给定channel的订阅链表中
通过PUBLISH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者
Pub/Sub从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以针对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用做实时消息系统,比如普通的即时聊天,群聊等功能
首先我们先开启四个xShell的客户端
我们的目标是启动三个redis集群服务,分别为6379、6380、6381端口
首先我们修改一下redis6379端口的配置文件
vim redis6379.conf
如此类推继续创建redis6380.conf、redis6381.conf
然后我们就可以启动啦啦啦啦啦啦啦啦啦啦啦啦
redis-server conf/redis6379.conf
redis-server conf/redis6380.conf
redis-server conf/redis6381.conf
高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间(比如server宕机、网络断联等)。
Redis主要提供一主多从下的主从复制和哨兵集群机制保证高可用。
单机的 Redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。
因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发。
读写分离
读操作:主库、从库都可以接收;
写操作:首先到主库执行,然后,主库将写操作同步给从库
主从复制,是将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave)。
数据的复制是单向的,只能由主节点到从节点。
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
首先我们默认访问三个redis服务,查看它们的详细信息
发现他们默认自己都是主机
那我们现在就来配置一下他们主机和从机之间的关系
主机:6379
从机:6380、6381
那我们现在只需要在从机6380、6381上面执行以下命令,那就是认老大 ⛹️ ⛹️♂️
SLAVEOF 127.0.0.1 6379
执行结果如下:
我们可以看到,这里他们的角色就转变成为小弟了,认老大成功!!!♀️ ♂️
其实在真是的开发情况下,我们都是在配置文件中进行配置的,这样是永久的。
我们进入从机的配置文件中可以看到
这里我们可以直接在配置文件中进行配置,那么当以后一启动就会找到主人了。
从这里我们可以看到主机写入,从机可以读取得到,但是从机不能够进行写入操作。
延申测试:主机断开连接,从机依旧连接到主机,但是没有写操作了,这个时候,主机如果回来了,从机依旧可以直接获取到主机的写操作。
其中如果从机退出重启的时候,如果在配置文件中有主机的信息,这样子就会对主机中的数据进行全量复制。
拓展问题:如果主机挂了,老大挂掉了,这样子应该怎么办呢,因为从机只能进行读业务,主机挂了,没有机子进行写业务了。但是我们也可以通过手动把6380变成主机,手动去把6380变成老大,但是如果在线上生产环境当中,这样的操作太麻烦了,于是乎就有了哨兵模式…
哨兵模式是一种特殊的模式,Redis 为其提供了专属的哨兵命令,它是一个独立的进程,能够独立运行。下面使用 Sentinel 搭建 Redis 集群,基本结构图如下所示:
在上图过程中,哨兵主要有两个重要作用:
第一:哨兵节点会以每秒一次的频率对每个 Redis 节点发送PING命令,并通过 Redis 节点的回复来判断其运行状态。
第二:当哨兵监测到主服务器发生故障时,会自动在从节点中选择一台将机器,并其提升为主服务器,然后使用 PubSub 发布订阅模式,通知其他的从节点,修改配置文件,跟随新的主服务器。
在实际生产情况中,Redis Sentinel 是集群的高可用的保障,为避免 Sentinel 发生意外,它一般是由 3~5 个节点组成,这样就算挂了个别节点,该集群仍然可以正常运转。其结构图如下所示:
上图所示,多个哨兵之间也存在互相监控,这就形成了多哨兵模式,现在对该模式的工作过程进行讲解,介绍如下:
对上对述过程做简单总结:
Sentinel 负责监控主从节点的“健康”状态。当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端来连接 Redis 集群时,会首先连接 Sentinel,通过 Sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 Sentinel 要地址,Sentinel 会将最新的主节点地址告诉客户端。因此应用程序无需重启即可自动完成主从节点切换。
在配置文件目录下
vim sentinel.conf
配置我们的主节点
# sentinel monitor 监控的名称 host port 1
sentinel monitor myredis 127.0.0.1 6379 1
后面的这个数字1,代表主机挂了,slave投票看让谁来接替成为主机,票数最多的就会成为主机!
配置完后,我们就可以启动哨兵啦啦啦啦啦啦
redis-sentinel conf/sentinel.conf
启动好了sentinel以后,我们就可以在6379上面shutdown一下,看一下监控的状态了
shutdown
sentinel监控结果:
优点:
1、哨兵集群,基于主从复制模式,所有的主从配置的优点,它都有。
2、主从可以切换,故障可以转移,系统的可用性更好。
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点:
1、Redis不好在线扩容,集群容量一旦达到上限,在线扩容就十分麻烦!
2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
当查询Redis中没有的数据时,该查询会下沉到数据库层,同时数据库层也没有该数据,当这种情况大量出现或被恶意攻击时,接口的访问全部透过Redis访问数据库,而数据库中也没有这些数据,我们称这种现象为"缓存穿透"。缓存穿透会穿透Redis的保护,提升底层数据库的负载压力,同时这类穿透查询没有数据返回也造成了网络和计算资源的浪费。
解决方案:
在接口访问层对用户做校验,如接口传参、登陆状态、n秒内访问接口的次数;
利用布隆过滤器,将数据库层有的数据key存储在位数组中,以判断访问的key在底层数据库中是否存在;
基于布隆过滤器,我们可以先将数据库中数据的key存储在布隆过滤器的位数组中,每次客户端查询数据时先访问Redis:
1、如果Redis内不存在该数据,则通过布隆过滤器判断数据是否在底层数据库内;
2、如果布隆过滤器告诉我们该key在底层库内不存在,则直接返回null给客户端即可,避免了查询底层数据库的动作;
3、如果布隆过滤器告诉我们该key极有可能在底层数据库内存在,那么将查询下推到底层数据库即可;
布隆过滤器有误判率,虽然不能完全避免数据穿透的现象,但已经可以将99.99%的穿透查询给屏蔽在Redis层了,极大的降低了底层数据库的压力,减少了资源浪费。
缓存雪崩是缓存击穿的"大面积"版,缓存击穿是数据库缓存到Redis内的热点数据失效导致大量并发查询穿过redis直接击打到底层数据库,而缓存雪崩是指Redis中大量的key几乎同时过期,然后大量并发查询穿过redis击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。事实上缓存雪崩相比于缓存击穿更容易发生,对于大多数公司来讲,同时超大并发量访问同一个过时key的场景的确太少见了,而大量key同时过期,大量用户访问这些key的几率相比缓存击穿来说明显更大。
解决方案:
1、在可接受的时间范围内随机设置key的过期时间,分散key的过期时间,以防止大量的key在同一时刻过期;
2、对于一定要在固定时间让key失效的场景(例如每日12点准时更新所有最新排名),可以在固定的失效时间时在接口服务端设置随机延时,将请求的时间打散,让一部分查询先将数据缓存起来;
3、延长热点key的过期时间或者设置永不过期,这一点和缓存击穿中的方案一样;