NoSQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,如商品网站中商品数据频繁查询
、对热搜商品的排行统计
、订单超时问题
、微信朋友圈音频、视频的存储
等,非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,特别是大数据应用难题。
RDBMS:关系型数据库,sql语句
NoSQL:Not only Sql 不仅仅是sql 非关系型数据库
全称Key-Value,这应该是我们都熟悉就像Map一样。代表数据库就是redis,redis的value还分了很多结构,例如:list、set、sorted set、hash、string等。
它是存储在内存中的,所以速度快常用来作为缓存服务器。 而且因为它的结构导致有些操作比关系型数据库简单。
举个例子例如List的[LPUSHX key value]操作,将一个值插入到已存在的列表头部,列表是有序的,如果在关系型数据库中得怎么办,插入一条数据,并且将控制位置的那个字段例如叫index,设为1。那是不是还得修改本来的那些数据,把后面所有行的index值都加一,这样才能控制有序,之后删除哪条数据,还得维护修改index。操作是比较麻烦的。
但是它ACID事务只支持I和C也就是隔离性和一致性,不支持原子性和持久性。所以在一些对事务要求的情况下就不适合了。
也就是按列来存储数据,关系型是按行存储。
按行存储的好处是业务可以简单的获取一行也就是多个列的数据,因为按行存储数据都是连续的,所以磁盘一次操作就读取所有列的数据。
但是按列的话,因为列的存储是不连续的,所以磁盘读取效率比行低
按行存储写如果操作也是一行一起的,保证的所有列的数据要么都成功写入,要么的失败。
如果是按列的话就有可能有些列成功,有些列失败。
但是在大数据统计的时候,一般就统计某一列或者某几列的数据。如果这时候是按行存储的话,那么每次从磁盘读取到内存时都会读取整行数据导致IO过大和资源的浪费。
所以节省I/O就采用按列存储,这样每次只需要拿想要的列进行统计。
代表的数据库是HBase,多用于离线的大数据分析和统计。为啥离线?上面说了写的操作可能会有问题,并且整行读的效率低,所以一般都是线上数据拷过来弄成列数据库,专门用户数据分析。
这个类型它的结构没有约束,可以存储任意结构,因为是文档嘛。啥意思呢,就是例如关系型数据库中规定这个表字段就两个,一个id,一个name。如果你想存个sex字段你就得修改表结构。那文档型不用,因为文档型存储的数据格式一般都是Json,Json里面的字段我任意填,无拘无束
而且这种类型的数据库容易存复杂的结构,因为Json是一种强大的描述语言,可以清楚的描述复杂的数据结构。如果复杂的数据结构放到关系型数据中那可能就得分很多表。例如我用户基本信息一个表、用户爱好的电影一个表,用户爱好的音乐一个表、用户爱好的游戏一个表,巴拉巴拉的,这些Json就能一次性搞定不需要分这么多表。
代表的数据库有MongoDB。3.2之前的版本不支持join操作,之后出了个lookup来实现join操作。4.0版本之前是不支持事务的,之后虽说支持事务,但是业界还是很少用它来保证事务的。
图数据库允许我们将数据以图的方式储存。实体会被作为顶点,而实体之间的关系则会被作为边。比如我们有三个实体,Steve Jobs、Apple和Next,则会有两个“Founded by”的边将Apple和Next连接到Steve Jobs。
代表的数据库有Neo4J、Infinite Graph、OrientDB。它适合在在一些关系性强的数据中,以及推荐引擎。如果我们将数据以图的形式表现,那么将会非常有益于推荐的制定。它不适合数据模型。图数据库的适用范围很小,因为很少有操作涉及到整个图。
Redis是一个开源的、遵循BSD的内存数据存储,常用作数据库、缓存、消息中间件
Redis数据存在内存中,所以读写快,机制:持久化机制 内存数据定期写入磁盘中
Redis是一个内存型数据库
# 联网情况下,执行如下命令进行下载
wget http://download.redis.io/releases/redis-6.0.10.tar.gz
# 一般都在usr下创建一个目录,方便管理
mkdir /usr/local/src/redis
# 切换到下载的目录,进行解压缩
tar -zxvf redis-4.0.2.tar.gz
# 将解压缩的redis移到到/usr/local/src/redis
mv redis-6.0.10 /usr/local/src/redis
# 进入文件夹内,进行编译并安装
make
make install
# 启动redis
redis-server
# 连接redis
redis-cli
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。
可以通过配置设置自动做快照持久化的方式。我们可以配置redis在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置
900秒内超过1个key被修改,则发起快照保存
300秒内超过10个key被修改,则发起快照保存
client 也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求。所以不推荐使用。
另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不 是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。
redis会将每一个收到的写命令都通过write函数追加到文件中(默认是 appendonly.aof)。
当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于os会在内核中缓存 write做的修改,所以可能不是立即写到磁盘上。这样aof方式的持久化也还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要 通过fsync函数强制os写入到磁盘的时机。有三种方式如下(默认是:每秒fsync一次)
aof 的方式也同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用incr test命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100就够了。
为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。收到此命令redis将使用与快照类似的方式将内存中的数据 以命令的方式保存到临时文件中,最后替换原来的文件。
需要注意到是重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
配置Redis的redis.conf
## 确保客户端可以访问到redis
bind 0.0.0.0
protected-mode no
daemonize yes
SpringBoot中关于redis的配置
# redis配置
Spring:
redis:
host: Redis所在的ip地址
port: 端口号
database: 选择的数据库
具体操作如下:
@SpringBootTest
class Boot06RedisApplicationTests {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisTemplate;
@Test
// 操作stringRedisTemplate字符串
void test1() {
//对于key、value都是String时,可以使用StringRedisTemplate
stringRedisTemplate.opsForValue().set("age", "32");
stringRedisTemplate.opsForValue().set("name", "李哥");
stringRedisTemplate.opsForList().leftPushAll("lists", "java", "python", "golang", "c++");
stringRedisTemplate.opsForHash().put("mainKey", "secondKey", "value");
stringRedisTemplate.opsForSet().add("city", "beijing", "xian", "shanghai");
stringRedisTemplate.opsForZSet().add("score", "lige", 100);
}
@Test
// 操作redisTemplate字符串
void test2() {
// 将key和hashKey的序列化方式设置位String,防止乱码,默认是JdkSerializationRedisSerializer
// 对于key、value存在对象时,可以使用RedisTemplate
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.opsForValue().set("user", new User(1, "lige", "陕西省"));
// 其余操作都类似于StringRedisTemplate
redisTemplate.opsForValue().set("age", "32");
redisTemplate.opsForList().leftPushAll("lists", "java", "python", "golang", "c++");
redisTemplate.opsForHash().put("mainKey", "secondKey", "value");
redisTemplate.opsForSet().add("city", "beijing", "xian", "shanghai");
redisTemplate.opsForZSet().add("score", "lige", 100);
}
@Test
// 对同一个key多次操作时,可以使用boundXxxOps绑定一个key
void test3() {
BoundValueOperations name = redisTemplate.boundValueOps("name");
name.set("changed lige");
name.append("zhenshuai");
}
}
(1)什么是缓存?
缓存就是计算机内存中的一段数据
(2)内存中数据特点
读写快、断电立即丢失
(3)缓存解决了什么问题?
(4)注意点
使用缓存时一定是数据库中数据极少发生修改,更多用于查询的操作
(5)本地缓存和分布式缓存区别?
本地缓存:存在应用服务器内存中的数据称之为本地缓存
分布式缓存:存储在当前应用服务器内存之外的数据称之为分布式缓存
集群:将同一种服务的多个节点放在一起共同对系统提供服务过程称之为集群
分布式:有多个不同服务集群功能对系统提供服务这个系统称之为分布式系统
自定义Cache类:
@SuppressWarnings("all")
public class RedisCache implements Cache {
private RedisTemplate redisTemplate;
// id:对应mapper的namespace="com.atlige.boot.mapper.UserMapper"
private final String id;
public RedisCache(String id){
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
// 使用redis里的hash类型作为缓存存储模型,key -- id, hashkey--当前方法key, value -- value
getRedisTemplate(redisTemplate).opsForHash().put(id.toString(), getKeyToMD5(key.toString()), value);
}
@Override
public Object getObject(Object key) {
// 根据key从redis的hash类型中获取数据
return getRedisTemplate(redisTemplate).opsForHash().get(id.toString(), getKeyToMD5(key.toString()));
}
/**
* 根据指定的key删除缓存
* @param key
* @return
*/
@Override
public Object removeObject(Object key) {
return null;
}
/**
* 清空缓存
*/
@Override
public void clear() {
// 清空namespace
getRedisTemplate(redisTemplate).delete(id.toString());
}
/**
* 计算缓存数量
* @return 缓存数量
*/
@Override
public int getSize() {
// 获取hash中的key、value的数量
return getRedisTemplate(redisTemplate).opsForHash().size(id.toString()).intValue();
}
public RedisTemplate getRedisTemplate(RedisTemplate redisTemplate){
if (redisTemplate == null){
redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
}
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
private String getKeyToMD5(String key){
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
注意点:
@Autowired
ApplicationContextUtils
来获取/**
* @author Jeremy Li
* @data 2021/1/22 - 22:39
* 用来获取springboot创建好的工厂
*/
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
//保留下来的工厂
@Autowired
private static ApplicationContext applicationContext;
// 将创建好的工厂以参数的形式传递给这个类
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtils.applicationContext = applicationContext;
}
// 提供在工厂中获取对象的方法
public static <T> T getBean(String beanName, Class<T> requiredType){
return (T) applicationContext.getBean(beanName);
}
}
增删改操作都会进入RedisCache的clear()中,进行清空该id下的缓存
<cache-ref namespace="com.atlige.boot.mapper.UserMapper"/>
优化:对放入redis的key进行优化,key的长度不能太长
(1)缓存穿透/击穿
客户端查询一个数据库中没有的数据记录导致缓存在这种情况下无法使用
mybatis中cache解决了缓存穿透:将数据库中没有查询到的结果也进行了缓存
(2)缓存雪崩
在系统运行的某一个时刻,突然系统中缓存突然失效,恰好在这一时刻涌来大量的客户端请求,导致所有模块缓存无法使用,大量请求涌向数据库导致极端情况,数据库阻塞或挂起。
缓存存储时:业务系统非常大,模块多,业务数据不同。不同模块放入缓存时,都会设置不同的缓存超时时间
解决方案:1.缓存永久存储(不推荐)2.不同模块放入缓存时,都会设置不同的缓存超时时间
主从复制架构仅仅用来解决数据的冗余备份,从节点仅仅用来同步数据
无法解决:1.master节点出现故障的自动故障转移
### master 7001
bind 0.0.0.0
port:7001
daemonize yes
protected-mode no
### slave1 7002
bind 0.0.0.0
port:7002
daemonize yes
protected-mode no
replicaof 192.168.152.128 7001
### slave2 7003
bind 0.0.0.0
port:7003
daemonize yes
protected-mode no
replicaof 192.168.152.128 7001
实现数据共享,但是从节点只能进行read操作
Sentinel是Redis的高可用性能解决方法:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器以及这些主服务器下的从服务器,并在被监视的主服务器进行下线状态时,自动将从服务器升级为新的主服务器。
哨兵就是带有自动故障转移的主从架构
无法解决:1.单节点并发压力问题 2.单节点内存和磁盘物理上限问题
创建一个sentinel文件夹并创建一个sentinel.conf
sentinel monitor [主从架构名称:mymaster] [主服务器的ip:192.168.152.128] [端口号:7003] [设置哨兵的数量:1]
使用redis/bin/下的redis-sentinel 加载配置文件并启动哨兵即可
(1)集群细节
CRC16算法:
相关的linux命令:
# 对每一个redis.conf进行修改
bind 0.0.0.0
port xxxx
daemonize yes
pidfile redis_xxxx.pid
logfile redis_xxxx.log
appendonly yes
cluster-enabled yes
cluster-config-file nodes-xxxx.conf
cluster-node-timeout 5000
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes
# 构建集群
redis-cli --cluster create 192.168.152.128:8000 192.168.152.128:8001 192.168.152.128:8002 192.168.152.128:8003 192.168.152.128:8004 192.168.152.128:8005 --cluster-replicas 1
# 查看集群状态
redis-cli --cluster check 192.168.152.128:8001
redis的session管理是利用spring提供的session管理解决方案,将一个应用session交给redis存储,整个应用中所有的session的请求都会去redis中获取相应的session数据
MSM:Memcached Session Manager
RSM:Redis Session Manager
步骤:
<!--springboot-redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring-data-redis session管理-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
@Configuration
@EnableRedisHttpSession // 将整个应用中使用session的数据全部交给redis处理
public class RedisSessionManager {
}
@Controller
@RequestMapping("test")
public class TestController {
@RequestMapping("test")
public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
List<String> list = (List<String>) request.getSession().getAttribute("list");
if (list == null){
list = new ArrayList<>();
// request.getSession().setAttribute("list", list);
}
list.add("xxxx");
request.getSession().setAttribute("list", list); // 每次session变化都要同步session
response.getWriter().write("size: " + list.size() + "\n");
response.getWriter().write("sessionId: " + request.getSession().getId());
}
@RequestMapping("logout")
public void testLogout(HttpServletRequest request){
request.getSession().invalidate();
}
}
== null){
list = new ArrayList<>();
// request.getSession().setAttribute(“list”, list);
}
list.add("xxxx");
request.getSession().setAttribute("list", list); // 每次session变化都要同步session
response.getWriter().write("size: " + list.size() + "\n");
response.getWriter().write("sessionId: " + request.getSession().getId());
}
@RequestMapping("logout")
public void testLogout(HttpServletRequest request){
request.getSession().invalidate();
}
}