谷粒商城项目笔记

发布时间 2023-09-18 15:52:36作者: 叫授_pront

项目地址传送门

前台笔记


Vue

ref 属性

ref 是 Vue 提供的一个 attribute,用于给元素或子组件注册引用信息。 通过 ref 我们可以在 Vue 的实例上访问这个元素或组件。

使用ref 的常见方式

  1. 引用一个子组件 <comp ref="comp"></comp>
  2. 然后在父组件通过 this.$refs.comp 访问子组件: this.$refs.comp.someMethod()

props 属性

props 是 Vue 中用来传递数据的一种方式。 用 props 的主要目的是让组件间的数据流动变成单向下行的。子组件可以读取父组件的数据,但不可以修改它。 使用 props 的方式是:在子组件中声明 props ,然后父组件通过 v-bind 指令将数据传给子组件。

前端难点

  1. 在观看前端代码的时候,公共模块里面传递数据用了两种方式,一种是emit 传递,一种是用Pubsub 传递,在观看商品模块前边代码的时候,觉得Pubsub 多余,并且我并没有导包,导致Pubsub 未定义报错,于是我将这行代码注掉了,但是看到后面就出问题了,出现了大量的消息订阅。这时候,我才将Pubsub 下载下来使用,但是有一个选择框始终发送不了请求,经过Apifox 接口测试发现能拿到正确的数据。并且在这个选择框的vue文件中发现了一个消息订阅事件。于是一一搜索发布这个事件的语句。但是在这个模块都没有找到。于是将错误定义到公共模块中。最后这个发布事件的语句刚好是我注释掉的一行代码.

后台笔记


服务器配置

nacos 本机

nginx 本机 ——> (迁移到云服务器中) 操作文档

mysql 腾讯云

elasticsearch 腾讯云

在更换地点的时候一定要更换localhost的ip地址,在/data/nginx/conf/conf.d/gulimall.conf --> proxy_pass

gulimall.priv 首页
search.gulimall.priv / gulimall.priv.search 检索页面

在访问页面的时候输入gulimall.priv, 首先交给腾讯云服务器nginx 进行解析转发给netapp 内网穿透代理服务器,最后访问内网特定端口(P139,P140)

项目整体评价

其实走到这里就发现,➡️项目大部分的CRUD都是很简单的。无非就是调方法,写SQL这个项目最复杂的数据库操作也是操作11张表,复杂是复杂了一点,但是原理是很简单的

✔️这时候就会出现更复杂的问题,比如说性能测试,压力测试。通过高并发的压力来排查业务的各种问题,是业务逻辑可以简化?多次的查询DB,某个中间件太慢?内存不够?服务器带宽太小?查询的不是数据库的索引?这些测试。于是就引发了项目整体架构的改变(更换中间件),SQL优化业务优化设置缓存jvm调优高并发下锁的设置

JVM

JVM 基本流程

java 文件首先会被编译为class 文件,然后通过类装载器传输到JVM中,所有的数据都在jvm中的运行时数据区里面,项目优化的大部分都在运行时数据区里面,然在就由JVM的执行引擎来执行,在运行时数据其的虚拟机栈里面进行方法调用,入栈/出栈等等操作,如果要调用本地接口方法的话。这些本地方法指定就是操作系统暴露的接口以及本地方法库。包括程序走到哪里的,方法走到哪一行了,这个是由JVM中的程序计数器监控的,每一个线程都有自己独立的虚拟机栈,本地方法栈,程序计数器,方法区和堆内存是共享数据

设计模式


单例模式

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

https://cdn.nlark.com/yuque/0/2023/png/34279121/1690938728751-a3f0857c-257d-4435-9574-ba769ac1632b.png#averageHue=%23f4f3f3&clientId=u3fc54736-156b-4&from=paste&height=260&id=ub4f3749a&originHeight=336&originWidth=592&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=51827&status=done&style=none&taskId=ub696e717-9892-492a-aa68-6e49d117ade&title=&width=457.6000061035156

在类里面创建实例而不是在类外创建实例

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决: 一个全局使用的类频繁地创建与销毁。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

  • 汉式

在真正需要使用对象时才去创建该单例类对象 懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。

public class Singleton {  
    private static Singleton instance;   
    private Singleton (){}  //构造方法私有化   
    public static Singleton getInstance() {  //只能调用getInstance方法      
        if (instance == null) {         
            instance = new Singleton();      
        }        return instance;    
    }
}

https://cdn.nlark.com/yuque/0/2023/png/34279121/1690938846243-9f04f3ed-e1ed-474d-981c-6902ee365d1c.png#averageHue=%23f3f3f3&clientId=u3fc54736-156b-4&from=paste&height=346&id=ub73beb49&originHeight=433&originWidth=398&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=35707&status=done&style=none&taskId=u8d933931-c07d-417f-8e1e-986851fdc98&title=&width=318.4

  • 饿汉式

饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。

public class Singleton{   
    private static final Singleton singleton = new Singleton();  
    private Singleton(){}   
    public static Singleton getInstance() {      
        return singleton;    
    }
}

注意上面的代码在第3行已经实例化好了一个Singleton对象在内存中,不会有多个Singleton对象实例存在

类在加载时会在堆内存中创建一个Singleton对象,当类被卸载时,Singleton对象也随之消亡了

缓存

为什么使用缓存机制

  • 可以通过优化业务逻辑来增加他的吞吐量,但是这种方式是有一定的上限的。在更多的时候,对于一些复杂的业务,已经不能通过优化简单的业务逻辑调整他的性能参数以增加它的吞吐量,比如一些复杂的Query,这些Query结果是恒定的。这时候就需要缓存机制,在分布式系统中,合理的使用缓存机制,就能极大的提升系统的性能。结果就是db承担数据落盘工作(持久化),持久化的保存到一个地方,为了提高系通的第一次访问,可以在第一次查询到数据的时候就放入缓存里面,以后从缓存中获取。

https://cdn.nlark.com/yuque/0/2023/png/34279121/1689994350482-40717287-1d82-4c50-b360-1aeb05077a11.png#averageHue=%23f6f3ec&clientId=u13190ad1-1541-4&from=paste&height=297&id=u24e77b1b&originHeight=422&originWidth=513&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=116462&status=done&style=none&taskId=u6e90fc80-a2cc-4f8f-a461-29d8ed345c0&title=&width=361.3999938964844

缓存数据一致性

如何保证缓存里面的数据和数据库的一致性? 常用的解决方法有如下三种

解决缓存一致性问题的方法

  • 双写模式

是在更改数据库的同时更改缓存

https://cdn.nlark.com/yuque/0/2023/png/34279121/1689994483689-e03b7646-cbcd-415d-9dcb-468fb6ba0951.png#averageHue=%23f1cdb4&clientId=u13190ad1-1541-4&from=paste&height=140&id=u26b76b40&originHeight=139&originWidth=446&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=33888&status=done&style=none&taskId=ubef8281f-80e9-4ba2-8244-403b5075cf5&title=&width=447.8000183105469

这种模式所面临的问题就是: 当有两个或者多个线程修改数据库的时候,第一个线程先写,但是操作完数据库由于各种原因操作缓存慢了半拍。第二个线程后写数据库,写完了马上就去写缓存了。最后第二个线程写完缓存第一个才写完,现在缓存中是一号线程的数据,但预期确实二号线程的缓存(二号线程最后操作数据库)

这是暂时性的脏数据问题,在数据稳定,缓存过期之后能得到正确的数据,如果对业务数据及时性要求不高可以忽略

  • 失效模式

是在更改数据库的同时将缓存删除

https://cdn.nlark.com/yuque/0/2023/png/34279121/1689994770819-eeb90014-597d-411e-9953-6a794b11dffb.png#averageHue=%23eecbb1&clientId=u13190ad1-1541-4&from=paste&height=198&id=u918a5531&originHeight=195&originWidth=449&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=49447&status=done&style=none&taskId=ub4e2d30b-9b7f-413b-9d6f-447449b3224&title=&width=455.20001220703125

这种模式所面临的问题就是: 当有三个及以上的线程同时操作DB,当一号线程写完DB并删除缓存,二号线程进来写DB,如果由于二号线程跑得慢,第三号线程来读缓存,由于一号线程写完DB之后删除了缓存,所以是没有缓存的,所以三号线程就去读数据库,二号线程还没有改完数据的,还没有提交最新的修改,相当于三号线程读到了老的一号线程的数据,然后三号线程更新缓存,但是如果说三号线程更新缓存的速度比较慢,在二号线程将自己的数据缓存删除了,现在缓存里就是一号线程的数据,按理来说应该是二号线程的数据

  • 使用cannel

https://cdn.nlark.com/yuque/0/2023/png/34279121/1689995616086-bb45cb88-7508-450e-bb27-e614f1cefb33.png#averageHue=%23e3c47e&clientId=u13190ad1-1541-4&from=paste&height=246&id=uc51ff058&originHeight=345&originWidth=651&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=101136&status=done&style=none&taskId=u8f611ee0-f62b-4313-9af6-efdc4fd3922&title=&width=463.79998779296875

使用cannel 去订阅DB,数据库更新通过binlog 缓存也会随之更改

使用缓存引发的问题

  • 缓存穿透

这个是高并发下缓存失效的问题

它是指一个一定不存在的数据,由于缓存时不命中的,将去查询数据库,但是数据库也没有这个记录,由于没有将这次查询的null结果写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

可以利用不存在的数据进行对服务器的攻击,使数据库的压力增大导致崩溃。

解决方式: null结果加入缓存,并加入短暂过期时间

  • 缓存雪崩

如果说给缓存中放入大量的数据,并都设置了同一的过期时间,然后在过期时间之后,所有数据全部从缓存中小时,下个时刻,数据库面临着巨大的压力,DB瞬间压力过重导致雪崩

解决方式: 在原有的失效时间基础上增加一个随机值,1-5min,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体的失效时间

  • 缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在耨写时间点呗超高并发的访问,这是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到DB身上。DB瞬间压垮

解决方式: 大量并发只让一个人去查询,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不同去查询DB

在实际应用中,往往会有多个线程同时访问同一个方法的情况,特别是在高并发的情况下,多个线程可能会同时请求同一个方法,这就会出现多个线程同时访问数据库的情况。

当多个线程同时访问数据库时,如果没有加锁控制,就有可能出现数据竞争问题,导致数据错误或者数据库压力过大等问题。因此,为了避免这种情况的发生,常见的做法就是在方法中加锁控制,保证同一时刻只有一个线程能够访问数据库。

如果是单例部署的话,锁加this 就没有毛病,因为SpringBoot 所有对象都是单例的只有一个实例,但是由于这个项目是分布式的,会部署的很多台机器,一个机器一个实例,100个机器100个实例,这样明显是行不通的(this)

本地锁的劣势就在于IO延迟时间远远大于锁的释放时间

Redis分布式锁

// 加锁成功...... 执行业务     
// 设置过期时间必须和加锁是同步的,原子的           
redisTemplate.delete("lock"); // 容易删除别人的锁         
String lockValue = redisTemplate.opsForValue().get("lock");        
if (uuid.equals(lockValue)) {
    //                删除自己的锁              
    redisTemplate.delete("lock");         
}           
// 获取值的对比和对比成功之后的删除 = 原子操作

Redisson

这是一个redis 分布式锁的框架对于redis分布式锁,更适合分布式下的场景

如果业务超长,运行期间会自动给锁续上30s(默认),不用担心业务时间失败,锁自动过期删除掉,加锁的业务只要运行完成,就不会给当前的锁续期,即使不手动解锁,锁默认会在30s 以后删除

如果传递了锁的超时时间,就发送给redis 执行lua脚本,进行占锁,默认的超时时间就是指定的时间 如果未指定锁的超时时间,就是用30 * 10000[LockWatchTimeout] 看门狗的默认超时时间 只要占锁成功,就会启动一个定时任务[重新给锁设置过期时间,新的过期时间就是看门狗的默认时间] 1/3的看门狗时间续期,续成满时间

读写锁

针对正在被改写的数据(且改写耗时,想拿到最新数据),让其被读阻塞等到写完成后,读最新数据。 保证一定能读到最新数据

@GetMapping("write")  
@ResponseBody    
public String writeValue(){    
    String s = "";        
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");    
    RLock rLock = lock.writeLock();       
    try{           
        //1.改数据加写锁 读数据加读锁            
        rLock.lock();          
        s = UUID.randomUUID().toString();       
        Thread.sleep(15000);         
        redisTemplate.opsForValue().set("writeValue",s); 
    }catch (InterruptedException e){      
        e.printStackTrace();    
    }finally {        
        rLock.unlock();    
    }        return s;   
}    
@GetMapping("read")   
@ResponseBody   
public String readValue(){    
    String s = "";      
    RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");     
    //加读锁       
    RLock rLock = lock.readLock();     
    try{          
        rLock.lock();       
        s = redisTemplate.opsForValue().get("writeValue");   
    }catch (Exception e){         
        rLock.unlock();          
        e.printStackTrace();     
    }        
    return s;   
}

读写锁: 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁: 写锁没释放,读锁就必须等待。

针对读数据时,只要当前正在修改数据,读数据就会等待写完成后,拿到最新数据。

Redisson 信号量

原理类似于只有所有人走完了门才能关, redisson信号量运用在秒杀业务,限流中,只有这么多信号量,用完之后想用必须等待。

具体在本项目中的实现就是下单扣减库存的操作中,为了避免频繁的与数据库交互,引入Redisson信号量机制,通过操作缓存,能够极大的提高系统性能。同时用户不用等待持久层操作完成,极大的提高用户的体验

线程池

随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。

线程池是什么 | 好处

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池的7大参数

  1. int corePoolSize: 核心线程数(一直存在,除非allowCoreThreadTimeOut)。 线程池,创建好以后就准备就绪的线程数量,就等待接收异步任务来执行 例如:corePoolSize:[5] 等于 5个 Thread thread =newThread()

  2. int maximumPoolSize: 最大线程数量。控制资源

  3. long keepAliveTime: 存活时间。如果当前的线程数量大于核心数量(corePoolSize) 释放空闲的线程(maximumPoolSize - corePoolSize)。 只要线程空闲大于指定的keepAliveTime,就会释放

  4. TimeUnit unit: 时间单位

  5. BlockingQueue

    workQueue: 阻塞队列。 如果任务有很多,就会将目前多的任务放在队列里面 只要有线程空闲,就会去队列里面取出新的任务执行

  6. ThreadFactory threadFactory: 线程的创建工厂

  7. RejectedExecutionHandler handler: 拒绝策略 如果队列满了,按照我们指定的拒绝策略拒绝执行任务

工作顺序

  1. 线程池创建,准备好 core 数量的核心线程,准备接受任务
  2. core 满了,就将再进来的任务放入阻塞队列中。空闲的 core 就会自己去阻塞队列获取任务执行
  3. 阻塞队列满了,就直接开新线程执行,最大只能开到 max 指定的数量
  4. max 满了,就会使用 RejectedExecutionHandler handler 指定的拒绝策略进行处理
  5. max都执行完了,有很多空闲。在执行的时间keepAliveTime以后,释放Max-core这些线程。最终保持到 core 大小
  6. 所有的线程创建都是由指定的 factory 创建的。

更多

更多原理如下地址

Java线程池实现原理及其在美团业务中的实践

JUC

JUC并发编程超详细详解篇_juc详解_白大锅的博客-CSDN博客

RabbitMQ

三大作用,流量处理,应用削峰,应用解耦

本地事务与分布式事务

本地事务


事务最大的特点是代理,事务生成的是代理对象,同一个方法内事务方法互调默认失效: 原因是绕过了代理对象,解决方案:使用代理对象调用事务方法

  1. **引入aop starte**r; spring-boot-starter-aop: 引入了aspectJ动态代理
  2. 在启动类添加 @EnableAspectJAutoProxy(exposeProxy=true // 对外暴露代理对象) 开启aspectj动态代理,以后所有的动态代理都是有aspectJ代理的而不是JDK的代理对象
  • 本地事务失效问题

    在分布式系统中,其中一个事务在本地完成了提交,但是由于各种原因(网络故障,节点故障,业务流程超长)该提交操作未被正确地传播到其他相关节点上。这种情况下,其他节点可能无法感知到该提交操作,导致数据一致性问题,本地事务失效,没有达到作用。

  • 本地事务失效的解决方式

    • 超时机制:参与者在等待最终提交通知时可以设置一个超时事件,如果超过该时间任未收到该通知,就可以主动终止事务。
    • 消息确认机制:协调者在发送最终提交通知后,要求参与者发送确认消息(消息队列),以确保消息正确接受
    • 日志和持久化:使用日志和持久化机制记录事务地状态和操作,在发生故障时进行恢复和回滚。

分布式事务


分布式系统经常出现的异常: 机器宕机,网络异常,消息丢失,消息乱序,数据错误,存储数据丢失,不可靠的TCP……

  • CAP定理
    • 一致性(Consistency):在分布式系统中所有的数据备份,在同一时刻是否有相同的值
    • 可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)
    • 分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信

CAP指的是,这三个点最多只能实现两点,不可能三者同时兼顾

Untitled

  • Base理论

    • 基本可用(Basecally Available):在分布式系统出现故障的时候,允许损失部分的可用性(响应实现,功能上的可用性),不等价于系统不可用
      • 响应时间上的损失: 正常情况下搜索引擎需要在0.5s之内给用户返回数据,如果出现故障(系统部分机房断电或者网络故障)响应时间可以酌情增加1~2s
      • 功能上的损失:购物网站在双十一,唯一保证系统的稳定性,部分消费者可能会被引导到一个降级页面
    • 软状态(Soft State):允许系统存在中间转台,而该中间状态不会应影响到系统整体的可用性看,分布式存储一般一份数据有多个备份,允许不同副本同步的演示就是软状态的体现
    • 最终一致性(Eventual Consistency):最终一致性是指系统中的所有数据副本经过一定的时间后,最终能够达到一致的状态。弱一致性和强一致性相反,做种一致性时弱一致性的一种特殊情况
  • 柔性事务(分布式事务的方案之一,本项目采用的方案)

    按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通知次数后即不再通知

  • 延迟队列以及死信队列

    • 延迟队列(Delay Queue):通常用于需要在一定时间延迟后执行的任务:
      • 定时任务:延迟队列可以用于执行定时任务:定时发送提醒通知,定时执行数据清理(在本项目中有体现)
      • 任务调度:延迟队列可以用于任务调度系统,按照执行的时间调度任务的执行
      • 重试机制:延迟队列可以用于实现任务的重试机制,当任务执行失败时候,可以将任务重新放入延迟队列,在一定的演示后再次尝试执行,需要手动开启ACK机制(在本项目有体现)
    • 死信队列(Dead Letter Queue):用于处理无法被正常消费的消息的队列。当消息无法被正常处理时,通常会被发送到死信独立额中,已便进一步处理或记录异常情况
      • 消息消费失败:在消费过程中发现异常或者处理失败是,可以将该消息发送的死信队列中。消费失败的原因包括消息格式错误,业务逻辑异常或者消费者无法处理的特殊情况
      • 消息超时:当消息在规定的时间内未能被消费者处理时,可以将该消息发送到死信队列中,超时可能时由于消费者故障,网络问题或者消费者处理能力不足造成的。
      • 队列溢出:当消息队列达到最大容量或者限制时,新的消息无法入队,这些无法入队的消息可以被发送到死信队列中。
  • 在分布式事务中的具体体现

    • 死信队列
      • 事务回滚: 当一个分布式事务中的某个参与者发生故障或者执行失败时,可以将该事务标记为死信的,并将相关的消息发送到私信队列中。其他参与者可以通过监听死信队列中来获取这些事务,并根据需要进行回滚或者进一步处理。
      • 异常处理:分布式事务中的某个参与者在处理消息可能会发生异常情况(数据校验失败,依赖服务不可用)这时,可以将该消息发送到死信队列,以便后续的异常处理和排查.
    • 延迟队列
      • 任务调度:延迟队列可以用分布式事务的调度和协调,例如,在一个分布式系统中,某个事务在未来的某个时间点执行,可以将该事务放入延迟队列中,并设置合适的延迟时间,当延迟时间达到时,事务会被触发执行。
      • 任务重试:反不是食物中的某个任务在执行过程中可能会失败(网络中断,依赖服务不可用)。延迟队列可以用于任务的重试机制,将失败的任务放入延迟队列,并在一定的延迟时间后重试执行

    在分布式事务中,死信队列和延迟队列可以结合使用,以处理异常情况和调度任务。当某个事务或任务无法正常执行时,可以将相关的消息或事务标记为死信,并发送到死信队列中。同时,延迟队列可以用于安排事务的延迟执行或任务的重试,以确保分布式系统的数据一致性和可靠性。具体的实现方式和配置取决于所使用的消息中间件或系统的特性和功能。

    分布式事务的实现方式


    1. 消息队列MQ
      1. 要求高并发的, 因为AT模式底层就是加锁,加各种锁,容易导致串行化,就不能使用AT模式
      2. 引入延时队列,当锁库存成功之后,将消息存储到延时队列中,延时队列将消息存储30min, 30min后,订单不支付,就关闭。30min后订单结果分晓,将消息发送给库存服务,库存服务如果查看订单早就没有了,或者订单没有支付,就将当时锁定的库存自动解锁 。使用这种方式回滚,达到订单失败了,库存自己解锁
    2. Seata
      1. seata 控制分布式事务, 每一个微服务先必须创建undo_log回滚日志表
      2. 不要求高并发的 使用Seata的AT模式解决 @GlobalTransactional

注解

@EnableTransactionManagement

在配置类上添加上这个注解为整个服务开启事务支持 等同于xml配置方式的

@Configuration
@EnableTransactionManagement //开启事务支持
public class MybatisPlusConfig {   
    //分页拦截器  
    @Bean   
    public MybatisPlusInterceptor mybatisPlusInterceptor() {    
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();       	
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));  
        return interceptor;   
    }
}

在Service 中使用@Transactional开启事务

@Service@Transactional(rollbackFor = Exception.class)
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>    implements ArticleService {    ...}

集合过滤–java 的流式方法

流式方法是Java 8新增的一种方法,它可以更方便地对集合进行过滤、映射和减少操作。

主要特点是:

  1. 返回一个流,而不是经过过滤/映射后的集合。
  2. 流是可以消费一次的。使用完之后就没用了。
  3. 流支持并行操作。

主要方法包括:

  • filter():过滤元素,保留符合条件的元素
  • map():对每个元素进行映射,转换为其他形式
  • limit():限制流的元素个数
  • skip():跳过某些元素
  • distinct():去除重复的元素
List<String> list = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
list.stream().filter(s -> s.isEmpty()).map(String::toUpperCase).forEach(System.out::println);//输出://// ABC// BC//// JKL

System.out::println 表示引用 System.out 类的 println() 方法。

方法引用用来引用现有的方法,它可以被像一个函数一样使用。

SpringGateway路由配置规则

Predicate 断言

Predicate 在这里指的是 Spring Cloud Gateway中的路由规则。

Gateway 使用 Predicate 来匹配进入的请求,如果匹配成功则进行路由。

主要的 Predicate 类型有:

  • Path:根据请求路径匹配,如 /foo/**
  • Query: 根据请求参数匹配,如 name=test
  • Header: 根据请求头匹配
  • Host: 根据 Host 头匹配
  • Method: 根据 HTTP 方法匹配,如 GET、POST 等
  • RemoteAddr: 根据请求的 IP 地址匹配

还有很多其他类型的 Predicate。

Predicate 使用 Java8 的函数式接口来定义,所以可以很方便地扩展新的 Predicate。

一个路由规则支持使用多个 Predicate,这些 Predicate 以 和(AND)的逻辑组合。

例如:

predicates:  - Path=/foo/**, /bar/**  - Query=name,test

上面路由规则表示匹配以下两种请求:

  1. 请求路径是 /foo/ 或者 /bar/ 开头
  2. 且请求参数中有 name=test

只有满足以上两个条件,请求才会被路由。

所以 Predicate 就是 Gateway 用来匹配和筛选请求的规则。配合路由目的地 URI 使用,可以实现很多复杂的路由需求。

路由规则的配置

Spring Cloud Gateway 支持多种路由规则:

  1. 基于URI的路由最简单的路由规则,根据请求的路径进行路由。路径以“/”开头。
- id: route_name  uri: lb://service-name
  1. 基于 Predicate 的动态路由使用 Predicate 配置动态路由规则。
- id: path_route  uri: lb://service-name  predicates:    - Path=/foo/**- id: query_route  uri: lb://service-name  predicates:    - Query=name,test
  1. 基于服务命名的路由根据请求头中的服务名路由。
- id: service_route  uri: lb://${requestHeader.X-Service-Name}
  1. 基于 Host 名的路由根据请求的 Host 头路由。
- id: host_route  uri: lb://${requestHeader.Host}
  1. 原生的Spring MVC路由使用Spring MVC路由规则,如:正则表达式、ant匹配器等。 ## Linux ### 常见命令
free -m 查看当前的空余内存
docker logs [name] 查看容器日志
chmod -R 777 /mydata/elasticsearch 修改权限
docker exec -it /bin/bash Linux 进入容器内部指令

业务流程

oss 存储服务的业务逻辑

在进行oss 服务的时候采用的有效策略是前台想服务器要一个加密签名,前台将加密签名和文件同时传递给oss服务器,oss服务器校验签名,存储文件,这样前台就不需要将文件上传至服务器占用带宽,服务器可以处理别的请求。

查询分组未关联的属性业务流程

  1. 前端先选择具体分组,将分组id: attrGroupId, 分页信息params, 提交给后端,祈求返回一个分页对象。
  2. 后端拿着attrGroupId 查到自己的分组对象
  3. 分组对象拿到自己的分类id : catelogId,并通过自己的分类id 拿到自己的分类对象
  4. 当前分类只能关联别的组没有引用的属性,所以先要把该分类下的所有分组信息查询出来返回的是一个List对象,里面封装了全部分组实体
  5. 通过stream流的方式拿到每一个分组的id 将他们封装为List ,这个集合里面装的是左右的分组id
  6. 找到这些分组关联的属性,从当前分类的所有的属性中移除这些这些属性,这一步需要在关联表中查询,因为只要分组有关联的属性就一定会在关联表中有记录,就查询分组id 在第四步的集合中的关联关系对象,会返回List 这个list 装在的所有当前分类下所有分组下关联的所有属性
  7. 将装在所有关联关系集合的List通过stream流的方式将每一个属性id 取出来映射为只装在属性id 的集合List
  8. 就构建查询条件,查询出分类id 是catelogId 并切类型是特定类型的所有属性,构造出这样一个查询对象
  9. 如果说装载所有属性id 的List不为空的话,就在查询条件对象后后面追加一个notIn 查询出不在所有分组关联的所有属性中存在的属性
  10. 同时,业务支持模糊检索功能,需要对页面的检索字段进行非空判断,如果不为空则多添加一个查询条件
  11. 最终将查询条件和分页对象封装为一个分页对象最终返回

支持模糊查询特定分类列表带分页业务流程

接口传递两个参数, 一个是

{ 
 page: 1,//当前页码  
 limit: 10,//每页记录数  
 sidx: 'id',//排序字段  
 order: 'asc/desc',//排序方式  
 key: '华为'//检索关键字
}

一个是key

由于需要从多个表中查询数据,具体到实现就是

  1. 先判断catelogId 是否为0,以此判断是全查询还是根据id 查询,构建QueryWrapper对象
  2. 判断key 值是否为空,以此判断是否开启模糊查询,构建QueryWrapper对象
  3. 构建Ipage 对象,通过Ipage 对象和wrapper对象创建出最终的Ipage 对象,由于Ipage 对象的泛型是AttrEntity, 但是页面展示的数据不止AttrEntity, 所以返回的并不是Ipage 对象,而是对Ipage 对象进行了一定的封装
  4. 这里采用了编写的工具类PageUtils,采用java流式方法将每一个AttrEntity一一映射到AttrResponseVo上,然后根据AttrEntity 身上的attr_id, 查询关联表中的数据,如果返回的关联表实体不为空,则通过关联表实体中的groupId 查询出对应的组名设置到AttrResponseVo
  5. 通过AttrEntity 身上的catelogId 属性查询出 所属的分类,将分类名称设置到AttrResponseVo
  6. 最终返回经过封装的Ipage 对象(pageUtils

商品最终上架业务流程(fixme)

  1. 通过页面拿到spu的id,然后通过stream流的方式映射通过spuId映射出所有的skuId,最后收集为一个只装载spuId 下所有skuId的集合
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
  1. 查询出spuId下所有可以用来被检索的属性,这些可以被检索的属性最终会存储的ES里面,具体操作步骤就是通过stream流的方式,将所有spuId基础属性的id封装为一个List<Long>集合,然后在这个集合里面过滤出可检索的属性,最后通过map映射的方式映射出具有检索属性的ES数据模型
//        在指定的所有属性集合里面,挑选出可检索的属性      
List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);  
Set<Long> idSet = new HashSet<>(searchAttrIds);       
List<SkuEsModel.Attrs> validIds = baseAttrs.stream().filter(item -> {      
    return idSet.contains(item.getAttrId());     
}).map(item -> {         
    SkuEsModel.Attrs attrs1 = new SkuEsModel.Attrs();       
    BeanUtils.copyProperties(item, attrs1);       
    return attrs1;       
}).collect(Collectors.toList());
  1. 发送远程调用,查询当前spuId商品是否有库存,是否有库存的标准是所有仓库库存总和加上所有锁定库存量 > 0

RabbitMQ在项目的体现

  1. 拥有两个交换机

    1. order-event-exchange:订单业务相关的交换机
      1. order.delay.queue:延迟队列/死信队列
      2. order.release.order.queue:关闭队列的一个队列
      3. order.seckill.order.queue:秒杀队列
      4. stock.release.stock.queue:解锁库存的一个队列
    2. stock-event-exchange:库存业务相关的交换机
      1. stock.delay.queue:延迟队列/死信队列
      2. stock,release.stock.queue:解锁库存的一个队列
  2. 死信队列: 在创建队列时创建两个参数

    1. x-dead-letter-exchange: 当消息在队列中变成死信时,接受死信消息的交换机
    2. x-dead-letter-routing-key: 消息以该路由键发送出去,可以由其他拥有该路由键的队列订阅捕获
  3. 延时队列:

    场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品

    常用解决方案:spring的schedule定时任务轮询数据库

    缺点:消耗系统内存,增加数据库压力,存在较大的时间误差

    解决:RabbitMQ的消息TTL的死信Exchange结合

    Untitled

    订单创建成功

    1. 订单创建成功之后会通过order.create.order路由键路由到order-event-exchange交换机。
    2. order.delay.queue这个死信队列兼延迟队列会订阅到这个消息,在30min过期之后会通过order.release.order路由键发送给order-event-exchange这个交换机。
    3. order.release.order.queue会通过相同的路由键订阅到这个消息。
    4. 订单业务相关监听器捕获到该消息判断是否需要进行释放订单。手动ack释放队列

    订单秒杀

    1. 秒杀时,系统通过order.seckill.order路由键发送给order-event-exchange交换机。
    2. order.sekill.order.queue通过该路由键订阅发送过来的消息
    3. 订单业务相关监听器捕获消息进行处理,手动ACK。

    解锁库存

    1. 在下单业务的时候就进行锁定库存服务。这里运用到了分布式事务。
    2. 通过stock.locked这个路由键发送给stock-event-exchange交换机。
    3. 消息被stock.delay.queue队列捕获,存活时间为50min,大于订单存活时间30min。存活时间一到,发送给stock-event-exchange交换机
    4. 被有着stock.release.#的stock.release.stock.queue队列捕获,库存服务相关的监听器会捕获到消息来查询订单的支付状态来进行是否需要解锁库存以及释放订单等等操作。

下单操作

  1. 进入后台的过滤器,如果没有登录则跳转到登录页面,反之,过滤器可以通过Sesssion获取到用户的基本信息存入到ThreadLocal对象中
  2. 获取到当前线程的ThreadLocal对象,之后进行验证令牌,验证价格,锁定库存,创建订单等相关操作。
  • 验证令牌

    1. 验证当前下单的商品token是否与redis中的存储的订单token一致,为了防止多人同时对一个订单操作,所以验证令牌必须使用lua脚本保证原子一致性,如果说验证失败将返回对象塞一个状态码表示令牌验证失败给前台展示。

    2.令牌验证成功之后进行订单验价,如果差距小于0.01(精度问题)说明验价成功,进行下一步操作,校验失败如上一样塞入状态码返回

  • 验证价格

    1. 验价操作第一步需要创建订单OrderEntity,生成随机的订单号码,设置进去(OrderEntity),拿到用户拦截器里面设置的用户信息,用户id设置进去。
    2. 拿到本类设置的ThreadLocal对象设置的订单确认信息OrderSubmitVo,在这里面拿到收货地址id,调用库存服务将运费,地址信息查询出来设置到OrderEntity中
    3. 远程调用购物车服务拿到所有的购物项将选中的购物项过滤出来以List形式返回
    4. 将订单数据(OrderEntity)和订单项数据(List)设置进OrderCreateTo中返回
    5. 计算价格以及积分相关的数据:遍历所有的订单项数据,累加所有价格为订单的总价格,累加所有优惠信息为订单的总优惠信息。
    6. 在OrderCreateTo中的价格和OrderSubmitVo(下单操作的参数)之间的差距如果小于0.01(精度问题)则任务验证价格成功,反之,验证失败,返回状态码信息
  • 锁定库存

    1. 如果说验证令牌成功,验价成功的话,就向数据库中保存订单,然后调用库存服务遍历每个仓库,查看是否有库存,如果每个仓库都没有该订单项的库存,则异常回滚订单操作以及库存操作,反之查询到有库存则进行锁定库存操作,下面是具体的业务逻辑
    2. 首先创建一个wareOrderTaskEntity订单库存工作单保存起来,是为了发生异常追溯那个工作单锁定几件库存后面方便解锁几件
    3. 遍历所有的订单项,每个订单项遍历所有的仓库查询是否有当前订单项的库存,如果所有商品在n个仓库中都有库存的话。进行锁定库存操作
    4. 通知MQ 库存锁定成功

各种配置文件区别与联系

联系:

  • 两种格式都用于配置Spring Boot应用
  • Spring Boot 可以同时读取两个文件,优先读取 application.properties
    1. application.properties 具有较高优先级,其中相同的属性值会覆盖 application.yml 中的配置。
    2. application.yml中的配置会作为默认值,被 application.properties 中的对应配置覆盖。

使用场景:

  • application.properties 简单直观,适用于少量配置
  • application.yml 常用于复杂对象的配置,层级结构清晰

配置绑定:

Spring Boot 能将这两种配置自动绑定到:

  • @Value注解
  • @ConfigurationProperties绑定的类
  • 命令行参数

bootstrap.properteis:

它有如下特点:

  1. 总是优先加载,甚至高于命令行参数。
  2. 只能包含 few key-valuepairs。
  3. 通常用于设置一些固定的配置,不希望被覆盖。

主要用于设置几个重要的配置:

  • spring.application.name:应用名称
  • spring.cloud.config.name:配置客户端名称
  • spring.cloud.config.profile:配置环境名称(默认为default)
  • spring.cloud.config.label:配置版本(分支)

这些配置会决定:

  • 从哪个配置服务器获取配置
  • 获取哪个环境(profile)的配置
  • 获取哪个版本(分支)的配置

优先加载 bootstrap 配置的原因是:

  • 通常用于设置重要的基础配置,如应用名称、配置环境等
  • 不希望被后面的配置覆盖

综上所述

特征 bootstrap.properties application.properties application.yml
格式 Properties Properties YAML
优先级 最高 较高 较低
作用 设置重要配置 普通应用配置 复杂对象配置
共存 可以 可以 可以
覆盖 会被后续配置覆盖 可以覆盖后续配置 可以覆盖后续配置
使用场景 设置基础配置 简单配置 复杂配置

JSR303

JSR303是Java领域中的一个标准,它定义了Bean Validation的规范。常用的一般用于表单的后台验证

它能用来验证Java对象(Bean)是否满足要求,包括:

  • 检查字符串长度合法 检查数值范围 检查Email格式 检查必填字段 检查列表、数组是否为空 自定义正则表达式验证
  • 表单数据验证 参数验证 服务接口入参验证 数据存储前验证

预备知识

  1. JSR303 是javax 包下面的,所以不用导入依赖,即开即用
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Null {   
    String message() default "{javax.validation.constraints.Null.message}";  
    Class<?>[] groups() default { };   
    Class<? extends Payload>[] payload() default { };  
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })        		 @Retention(RUNTIME)  
    @Documented   
    @interface List {    
        Null[] value();  
    }
}
  1. 每一个注解中都有3个属性
    1. message : 校验失败的信息,后面的字符串是从配置文件中取值,可以自己配置
    2. groups : 定义的组名,代表校验注解具体在那个分组下生效,可以配置多个分组,分组为接口类型
    3. payload : 不做要求
  2. 所使用的注解
    1. @Target 这个注解适用的类型
      • METHOD :可以用在方法或构造函数上
      • FIELD :可以用在字段上
      • ANNOTATION_TYPE :可以用在其他Annotation上
      • CONSTRUCTOR :可以用在构造函数上
      • PARAMETER :可以用在参数上
      • TYPE_USE :可以用在任意使用类型的地方
    2. @Retention:指定Annotation的生命周期,如 explained。
    3. @Repeatable:表示这个Annotation可以重复使用。
    4. @Documented:说明这个Annotation应该被javadoc工具记录。
    5. @Constraint:表示这是一个约束注解,需要一个ConstraintValidator来实现验证逻辑。
    6. validatedBy:指定相关的ConstraintValidator去验证这个Constraint。

自定义注解使用流程

@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {  
    String message() default "{priv.pront.common.validator.annotation.ListValue.message}";   
    Class<?>[] groups() default { };   
    Class<? extends Payload>[] payload() default { };
}
  1. 填写需要校验的类型,属性:
   int[] values() default {  };  // 表明只能适用values 里面列举的值
  1. 然后根据业务逻辑情况修改@Target 类型
  2. 填写自定义校验器(必须
@Constraint(validatedBy = { ListValueConstrainValidator.class })
public class ListValueConstrainValidator implements ConstraintValidator<ListValue, Integer> {
    private Set<Integer> set = new HashSet<>();

    //    !初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] values = constraintAnnotation.values();
        for (int value : values) {
            set.add(value);
        }
    }


    /**
     * !判断是否校验成功
     * @param value   需要校验的值
     * @param context context in which the constraint is evaluated
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}

     继承`ConstraintValidator `并实现方法
  1. 编写校验错误信息提示

https://cdn.nlark.com/yuque/0/2023/png/34279121/1685694583612-6982508f-8e21-458c-abe9-f635ea76cdff.png#averageHue=%23232528&clientId=uc82c9f4a-18a0-4&from=paste&height=506&id=u5e14da90&originHeight=632&originWidth=1286&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=49788&status=done&style=none&taskId=uaae76a91-bb1b-400e-a0b9-0e8df4df678&title=&width=1028.8

需要注意的是必须和String message() default "{priv.pront.common.validator.annotation.ListValue.message}"; 信息路径一致

SPU 与 SKU

  • spu是商品中的某一类别,描述了产品属性和特征,可能有多个规格和选项
  • sku是SPU下的具体商品,带有唯一条形码,具备特定的规格属性,是销售、进货的单位

它们在电商中分别代表:

  • SPU 描述了产品基本信息,包括名称、品牌、分类等
  • SKU 则代表实际可以购买的具体商品,并且具有唯一的ID用于统计和管理。 | 区别 | SPU(商品) | SKU(唯一商品) | | — | — | — | | 含义 | 最细粒度的完整产品 | 一个SPU在特定规格下的具体产品 | | 特点 | 包含多个规格参数 | 有唯一的物料编码(barcode) | | 应用 | 描述产品的基本信息 | 代表实际上上架销售的产品 |

HikariCP

HikariCP 是一款日本开源的 JDBC 连接池。HikariPool 是它的连接池实现。

Mybatis-Plus实现分页查询

分页类型

物理分页:相当于执行了limit分页语句,返回部分数据。物理分页只返回部分数据占用内存小,能够获取数据库最新的状态,实施性比较强,一般适用于数据量比较大,数据更新比较频繁的场景。

逻辑分页:一次性把全部的数据取出来,通过程序进行筛选数据。如果数据量大的情况下会消耗大量的内存,由于逻辑分页只需要读取数据库一次,不能获取数据库最新状态,实施性比较差,适用于数据量小,数据稳定的场合。

那么MP中的物理分页怎么实现呢? 往下看往下看

配置

@Configurationpublic class MyBatisPlusConfig {   
    /** 
    * 分页插件  
    * @return   
    */  
    @Bean  
    public PaginationInterceptor paginationInterceptor() {     
        return new PaginationInterceptor();    
    }
}

@ReuqestParam 和@ RequestBody 有什么区别和联系

用途不同:

  • @RequestParam用来获取请求参数,获取的是请求url、 forms表单以及请求头中的参数
  • @RequestBody用来获取请求体中的json数据
  1. 参数采用方式不同:
  • @RequestParam获取的参数是 nome1=value1&nome2=value2这种格式
  • @RequestBody获取的参数是{“key”:“value”}这种json格式
  1. Datatype :
  • @RequestParam接收的类型是String , Integer 等简单类型
  • @RequestBody接收的类型可以是对象,也可以是List、Map等结构

虽然功能不同,但他们也有联系: 1. 他们都是Spring MVC的注解,用于获取请求中的数据。 2. 他们在Controller层的方法参数中使用,可以为方法注入请求数据。 3. 可以配合@RequestMapping使用,用于映射URL到Controller中的具体方法。 4. 请求的数据最终都是由HttpServletRequest对象描述,只不过:@RequestParam从request parameters中获取,@RequestBody从request body中获取。

简而言之: - @RequestParam用于获取通过@RequestMapping映射的URL请求中的参数 - @RequestBody用于获取POST、PUT、PATCH请求中的json数据

基础知识

Controller 只处理请求,接受和校验数据 Service 接受controller传来的数据,进行处理 Controller 接受Service 处理完的数据,封装页面指定的vo

上网流程

  • 在浏览器输入框中输入域名的时候,首先通过电脑的host文件进行解析,如果该域名下存在ip地址,则有电脑的网卡进行跳转访问。
  • 如果电脑的host 文件中没有该域名对应的IP地址,就会联网查询DNS服务器(公网保存),查询该域名下面的IP

内存泄漏

对象申请完内存后无法合理的释放内存

代理方式

正向代理

正向代理一般的用处就是?上网,客户端想要访问Google,必须先访问代理服务器。通过代理服务器访问互联网,在互联网看到的都是代理服务器的IP地址,会隐藏掉客户端的信息,访问外网

反向代理

反向代理一般就是访问内网,因为项目的一些服务肯定是在内网部署的,外网是访问不到的,这时候就需要代理服务器,如Nginx ,他会将外网的请求向内网发。

中间件越多,性能损失越大,大多都损失在网络交互上面

后端难点

  • 在进行添加商品的时候,不仅要给本表中添加信息,还需要给pms_spu_imags, pms_spu_info_desc,sku_info,sku_sale_attr_value, sku_info着5张表添加。同时还需要进行跨库操作,进入到营销系统的数据库中,保存每个sku 的积分表,满减表,以及spu的各种信息。这不仅仅牵扯到数10张表的数据操作,还涉及到远程调用feign,同时因为数据量比较大,需要对事务,数据安全性进行一定的要求
  • 在商城业务中检索服务中对商品进行搜索的时候,不经要对keyword 进行模糊匹配,按照属性/分类/品牌/价格区间/库存 进行分类, 还要根据各种字段排序,高亮显示,同时,对es 里面所有能查到的数据进行聚合分析,将所有属性,品牌,排序字段,等等动态显示在页面上

数据库

卡迪尔积以及具体的处理方式

在数据库领域,卡尔德雷积(Cauchy product)常用于表连接操作。

-- INNER JOIN  
SELECT * 
FROM users
INNER JOIN orders  
ON users.id = orders.user_id

-- 多个 WHERE 
SELECT *
FROM users  
WHERE gender = 'M' AND age > 30

-- 多个 GROUP BY
SELECT COUNT(*)
FROM users  
GROUP BY gender, age

这些情况会导致卡迪尔积

冗余字段的意义和作用

创建多个表和冗余字段,在实际业务开发中有几个重要意义:

  1. 降低结构耦合度

将相关但不完全一致的数据分到不同表,可以降低表的复杂度,提升扩展性。

比如用户信息和订单信息,放在一个表将会很臃肿。分到不同表更合理。

  1. 提高数据独立性

将不同领域的信息分到不同表,可以更好地控制访问性和数据生命周期。

例如用户信息和产品信息,可以分开控制。

  1. 优化查询性能

将热点数据如订单信息分表,可以有效分摊负载,提高查询效率。

  1. 提供冗余字段

提供表内或表间冗余字段,可以提高查询性能。

如订单表中增加总金额字段,用户表冗余业务名称字段。

虽然存在数据不一致性的风险,但可以改善 join性能。

  1. 适应业务演进

随着业务不断演进,数据库结构和字段也需要增加。

增加新的表或字段,不影响旧字段和原有流程。

总的来说,数据库设计主要考虑:

  • 降低耦合

  • 提高数据独立性

  • 提升查询性能

  • 适应未来变更 ## pms 数据库各个表之间关系的梳理 > pms 共有如下15张表 > >

    https://cdn.nlark.com/yuque/0/2023/png/34279121/1685698546066-f7c2203a-5b9b-4b5f-84fa-789e395fe3b4.png#averageHue=%2333312f&clientId=ue56aeca3-0a2d-4&from=paste&height=283&id=u011aa285&originHeight=406&originWidth=338&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=11391&status=done&style=none&taskId=u81b1ca1b-4270-4776-81ec-fcf8defae7b&title=&width=235.39999389648438

    https://cdn.nlark.com/yuque/0/2023/png/34279121/1685699163573-b5765914-33af-4d9e-aefa-5da61d77f208.png#averageHue=%23dee9f2&clientId=ue56aeca3-0a2d-4&from=paste&height=618&id=u873ab79d&originHeight=1064&originWidth=857&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=151904&status=done&style=none&taskId=ub7aa6857-6799-4ae1-a899-10e69ec254d&title=&width=497.6000061035156

https://cdn.nlark.com/yuque/0/2023/png/34279121/1685699256720-10edb02f-d638-4617-8474-1106e446b2d1.png#averageHue=%23dac7a0&clientId=ue56aeca3-0a2d-4&from=paste&height=471&id=ua871e579&originHeight=944&originWidth=982&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=95363&status=done&style=none&taskId=u981d1005-1c20-4409-9a82-ba926b8f611&title=&width=490

image.png

https://cdn.nlark.com/yuque/0/2023/png/34279121/1685699518723-8e06e377-ccad-45d2-a446-57b0b6304b81.png#averageHue=%23c5cb84&clientId=ue56aeca3-0a2d-4&from=paste&height=355&id=u41e34353&originHeight=971&originWidth=1254&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=133544&status=done&style=none&taskId=ud81526e8-a942-4ccc-a592-9c5976bc09a&title=&width=459

image.png

  • pms_brand 这个品牌表要关联具体的分类也就是要关联 pms_category 这张表
  • 但是 pms_category 这张表数据量比较大,关联查询容易造成卡迪尔积。所以采用额外的一张表 pms_category_brand_relation 保存 具体的 品牌对应的分类, 将两者id , 两者常用属性作为表中的字段,使得联表查询的时候只需要查询pms_category_brand_relation一张表。

https://cdn.nlark.com/yuque/0/2023/png/34279121/1685700265887-0f73d43d-6d42-474d-9027-98878be8548c.png#averageHue=%23fefefd&clientId=ue56aeca3-0a2d-4&from=paste&height=586&id=u74063ecf&originHeight=732&originWidth=2503&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=121310&status=done&style=none&taskId=uc94d7d1e-637a-45a4-b78e-715dc9506ad&title=&width=2002.4

image.png

https://cdn.nlark.com/yuque/0/2023/png/34279121/1685701547268-10fc0c4d-cc96-401d-b58f-8f82fcaca114.png#averageHue=%23f8f8f8&clientId=ue56aeca3-0a2d-4&from=paste&height=678&id=u7cb833f3&originHeight=847&originWidth=1597&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=114295&status=done&style=none&taskId=u8e012ac2-3268-4cf7-ad31-e7bb1bd7e5b&title=&width=1277.6

image.png

Bug&Todo

public class Singleton { private static Singleton instance; private Singleton (){} //构造方法私有化 public static Singleton getInstance() { //只能调用getInstance方法 if (instance == null) { instance = new Singleton(); } return instance; }}