Redis
MySQL的数据是始终存在硬盘上的,对于我们的用户信息这种不需要经常发生修改的内容,使用MySQL存储固然可以,但是如果是快速更新或是频繁使用的数据,比如微博热搜,双十一秒杀,这些数据不仅要求服务器需要提供更高的响应速度,而且还需要面对短时间内上百万,甚至上千万次的询问,而MySQL的硬盘IO读写性能完全不能满足上面的要求,能搞满足上述需求的只有内存,因为速度远高于磁盘IO
因此,我们需要寻找一种更好的解决方案,来存储上述这类特殊资源,弥补MySQL的不足,以应对大数据时代的重重考验
NoSQL概论
NoSQL全称是 Not Only SQL , 它是一种非关系型数据库,相比传统 SQL 关系型数据库,它:
- 不保证关系数据的ACID特性
- 并不遵循SQL标准
- 消除数据之间关联性
优势:
- 远超传统关系型数据库的心梗
- 非常易于扩展
- 数据模型更加灵活
- 高可用
因此,NoSQL非常适用于高并发海量数据
Redis是NoSQL数据库的一种,是键值存储数据库: 所有的数据都是以建值方式存储的,类似于我们之前学过的HashMap,使用起来非常方便,性能也非常高
Redis是一个开源的键值存储数据库,所有的数据全部存放在内存中,它的性能大大高于磁盘IO,并且也可以支持数据持久化,它还可以支持横向扩展,主从复制等
实际生产中,我们一般配合使用Redis和MySQL,发挥它们各自的优势,取长补短
Redis安装和部署
Redis下载: https://github.com/tporadowski/redis/releases
下载完成后,解压缩,双击即可打开Redis
基本操作
在我们之前使用MySQL时,我们需要先在数据库中创建一张表,并定义好表的每个字段内容,最后通过 insert 语句向表中添加数据
而Redis 并不具有MySQL那样的严格的表结构,Redis是一个键值数据库,因此,可以向Map一样的操作方式,通过键值对向Redis数据库中添加数据库(操作起来类似于向一个HashMap中存放数据)
在Redis下,数据库是由一个整数索引来表示,而不是由一个数据库名称
默认情况下,连接Redis之后,会使用0号数据库,我们可以通过Redis配置文件中的参数来修改数据库总数,默认是16个
我们可以通过select语句来进行切换:
select 序号;
数据操作
我们来看看,如何向Redis数据库中添加数据
set <key> <value>
-- 一次性多个
mset [<key> <value>]...
所有存入的数据默认会以字符串的形式保存,键值具有一定的命名规范,以方便我们可以快速定位我们的数据属于哪一个部分,比如用户的数据:
-- 使用冒号来进行板块分割,比如下面表示用户XXX的信息中的name属性,值为lbw
set user:info:用户ID:name lbw
我们可以通过键值获取存入的值
get <key>
Redis还支持数据的过期时间设定
set <key> <value> EX 秒
set <key> <value> PS 毫秒
当数据达到指定时间时就会被自动删除
我们也可以单独为其他的键值对设置过期时间
expire <key> 秒
通过下面的命令来查询某个键值的过期时间还剩多少
ttl <key> //毫秒显示
persist <key> //将key转换为永久
那么当我们向直接删除这个数据时
del <key>...
当我们向查看数据库中所有的键值时
keys *
查询某个键是否存在
exists <key>...
随机拿一个键
randomkey
将一个数据库中的键移动到另一个数据库中
move <key> 数据库序号
修改一个键为另一个键
rename <key> <新的名称>
-- 下面这个会检查新的名称是否已经存在
renamex <key> <新的名称>
如果存放的数据是一个数字,我们还可以对其进行自增自减操作
incr <key> //++
decr <key> //--
incrby <key> b //key + b
数据类型介绍
一个键值对除了存储一个String类型的值以外,还支持多种常用的数据类型
Hash
这种数据类型本质上就是一个HashMap,也就是潜逃了一个HashMap罢了,在Java中就像这样:
#Redis默认存String类似于这样
Map<String,String> map = new HashMap<>();
#Redis存Hash就类似于这样:
Map<String,Map<String,String>> map = new HashMap<>();
它比较适合存储类这样的数据,添加一个Hash类型的数据:
hset <key> [<字段> <值>]...
hset person name lbw age 10
我们可以直接获取:
hget <key> [<字段> <值>]
--如果想要一次性获取所有的字段和值
hgetall <key>
判断某个字段是否存在:
hexists <key> <字段>
删除Hash中的某个字段
hdel <key> [field] [field...]
我们发现,在操作一个Hash时,实际上就是我们普通操作命令前面添加一个h,这样就能以同样的方式去操作Hash里面存放的键值对了,之力就不意义列出所有操作了
我们来看看及格比较特殊的
我们现在想要知道Hash中一共存了多少个键值对
hlen <key>
我们也可以一次性获取所有字段的值
hvals <key>
唯一需要质疑的是,Hash中只能存放字符串值,不允许出现嵌套的情况
List
像Java中的List,支持随机访问,双端操作,很像LinkedList
--向列表头部添加元素
lpush <key> <element>...
--向列表尾部添加元素
rpush <key> <element>...
--在指定元素前面/后面插入元素
linsert <key> before/after <指定元素> <element>
同样的,获取元素也非常简单
--根据下标获取元素
lindex <key> <下标>
--获取并移除头部元素
lpop <key>
--获取并移除尾部元素
rpop <key>
--获取指定范围的
lrange <key> start stop
注意,下标可以使用负数来表示从后到前数的数字
--获取列表a中的所有元素
lrange a 0 -1
push和pop还可以连着用
--从前一个数组的最后取一个出来放到另一个数据的头部,并返回元素
rpoplpush 当前数组 目标数组
Set和SortedSet
Set集合其实就像Java中的HashSet一样(HashSet本质上是利用了一个HashMap,但是Value是固定对象,仅仅是key不同)它不允许出现重复元素,不支持随机访问,但是能够利用hash表提供极高的查找效率
向Set中添加一个或多个值
sadd <key> <value>...
查看Set集合中有多少个值
scard <key>
判断集合中是否包含:
-- 是否包含指定值
sismember <key> <value>
-- 列出所有值
smembers <key>
-- 那些是集合a中有b中没有的
sdiff a b
-- 那些是集合a和集合b都有的
sinter a b
-- 集合ab的并集
sunion a b
-- 将集合的差集存到目标集合上
sdiffstore target a b
移动指定值到另一个集合中
smove <key> 目标 value
移除操作
srem <key> <value>...
那么如果我们要求Set集合中的数据按照我们指定的顺序进行排序怎么办呢?这时可以使用SortedSet,它支持我们为每个值设定一个分数,分数的大小决定了值的位置,所以它是有序的
我们可以添加一个带分数的值:
zadd <key> [<score> <value>]...
同样的:
-- 查询有多少个值
zcard <key>
-- 移除
zrem <key> <value>...
-- 获得区间内所有
zrange <key> start stop [with scores]
-- 获得成绩区间
zrangebyscore <key> min max [withscores]
-- 统计分数段内的数量
zcount <key> start stop
-- 根据分数获取指定值的排名
zrank key value
https://www.jianshu.com/p/32b9fe8c20e1
持久化
我们知道,Redis数据库中的数据都是存放在内存中,虽然很高效,但是这样存在一个非常严重的问题,如果突然停电,那我们的数据不就全部丢失了吗?不像硬盘上的数据,断电依然能够保存
这个时候我们就需要持久化,我们需要将我们的数据备份到硬盘上,防止断电或是机器故障导致的数据丢失
持久化有两种方案:
- 保存已存储的数据,相当于复制内存中的数据到硬盘上,需要恢复数据时直接读取即可
- 存储我们存放数据的所有过程,需要恢复数据时,只需要重复过程即可
RDB
就是我们说的第一套方案
save
-- 注意上面这个命令是直接保存,会占用一定的时间,也可以单独开一个子进程后台执行保存
bgsave
执行后,会在服务端目录下生成一个dumb.rdb文件,而这个文件中就保存了内存中存放的资源,当服务器重启后,会自动加载里面的内容
保存后我们可以关闭服务器
shutdown
我们重启客户端后数据依然存在
我们建议隔一段时间就进行保存(最好是自动的)
我们可以在配置文件上设置自动保存,并设定一段时间内写入多少数据时,执行一次保存操作
save 300 10 # 300秒(5分钟) 内有10个写入
save 60 10000 # 60s内有10000个写入
配置的save使用的都是bgsave后台保存
AOF
存储过程
但是我们多久写一次日志?我们有三种策略:
- always: 每次执行写操作都会保存一次
- everysec: 每秒保存一次
- no: 看系统心情保存
默认是关闭的
开启后默认是第二种策略
我们可以输入命令手动执行重写操作
bgrewriteaof
在配置文件中自动配置重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
- AOF 存储速度快,消耗资源少,支持实时存储,加载速度慢,数据体积大
- RDB加载速度快,数据体积小,存储速度慢,大量消耗资源,会产生数据丢失
事务和锁机制
和MySQL一样,在Redis中也有事务机制
-- 打开事务
multi
-- 提交事务
exec
-- 取消事务
discard
实际上整个事务是创建了一个命令队列,它不像MySQL那种在事务中也能单独取得结果,而是统一执行
锁
Redis的锁机制是乐观锁
- 悲观锁:禁止一切外来访问
- 乐观锁:直接对数据进行操作,在操作时判断是否有其他人抢占资源
Redis中可以使用watch来监视一个目标,如果执行事务之前被监视目标发生了修改,则取消本次事务
watch <key>
解决aba问题,不是根据值,而是根据版本号
使用Java和Redis进行交互
既然了解了如何通过命令窗口操作Redis数据库,那么我们如何使用Java来操作呢?
这里我们需要使用到Jedis框架,它能够实现Java和Redis数据库的交互 依赖:
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.0.0</version>
</dependency>
连接Redis数据库
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
//使用之后关闭连接
jedis.close();
}
通过jedis对象,我们就可以直接调用命令的同名方法来执行Redis命令了
记得开始运行程序前打开redis-server .exe 就是打开服务器
//创建Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.set("test","frank");//set test frank
System.out.println(jedis.get("test"));//get test
jedis.close();
得到这些,Java就成功连接到了Redis
Hash类型的数据也是这样:
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("127.0.0.1",6379);
/*jedis.set("test","frank");//set test frank
System.out.println(jedis.get("test"));//get test*/
jedis.hset("user","name","francis");
jedis.hset("user","id","1");
jedis.hgetAll("user").forEach((k,v)-> System.out.println(k + " : " + v));
jedis.save();
jedis.close();
}
列表操作:
jedis.lpush("myList","33","22","11");
List<String> myList = jedis.lrange("myList", 0, -1);
System.out.println(myList);
jedis.save();
jedis.close();
SpringBoot,整合Redis
我们接着来看如何在SpringBoot项目中整合Redis操作框架,只需要一个starter即可,但是它底层没有用Jedis,而是Lettuce
<!--配置Redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
或者直接用SpringBoot加载器,更方便
创建好项目,默认连接了本地Redis,端口号6379,0号数据库
等同于
等于说这里写了跟没写效果一样
starter已经给我们提供了两个默认的模板类
package org.springframework.boot.autoconfigure.data.redis;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
类中有许多封装好的方法
得到1000后,说明已正确连接Redis
那么如何去使用这两个模板类呢?我们可以直接注入StringRedisTemplate来使用模板
@SpringBootTest
class RedisSpringBootApplicationTests {
@Autowired
StringRedisTemplate template;
@Test
void contextLoads() {
ValueOperations<String, String> operations = template.opsForValue();
operations.set("c","xxxxx");//设置值
System.out.println(operations.get("c"));
System.out.println(template.delete("c"));//删除值
System.out.println(template.hasKey("c"));//判断是否含键
}
}
实际上所有的值的操作都被封装到了ValueOperations对象中,而普通的键操作直接通过模板对象就可以使用了,大致使用方式其实和Jedis一致
Redis与分布式
我们学习下Redis在分布式开发场景下的应用
主从复制
在分布式场景下,我们考虑让Redis实现主从模式
主从模式,是指将一台Redis服务器的数据复制到其他服务器,前者称为主节点,后者称为从节点
数据的复制是单向的,由主到从
- 实现了读写分离,提高了性能
- 在写少读多的情况下,我们甚至可以安排很多从节点,这样能够大幅度分担压力,并且就算挂掉几个,其他的也能使用
输入指令,看到端口号为6001的服务器的主从状态
输入指令,让6002为从,6001为主
我们发现从属数量变为1
主从偏移量相等,说明数据转移完全
输入命令:
replicaof no one
解除6002的所有主从关系
``
实际上所有的值的操作都被封装到了ValueOperations对象中,而普通的键操作直接通过模板对象就可以使用了,大致使用方式其实和Jedis一致
Redis与分布式
我们学习下Redis在分布式开发场景下的应用
主从复制
在分布式场景下,我们考虑让Redis实现主从模式
主从模式,是指将一台Redis服务器的数据复制到其他服务器,前者称为主节点,后者称为从节点
数据的复制是单向的,由主到从
- 实现了读写分离,提高了性能
- 在写少读多的情况下,我们甚至可以安排很多从节点,这样能够大幅度分担压力,并且就算挂掉几个,其他的也能使用
输入指令,看到端口号为6001的服务器的主从状态
输入指令,让6002为从,6001为主
我们发现从属数量变为1
主从偏移量相等,说明数据转移完全
输入命令:
replicaof no one
解除6002的所有主从关系