0%

业务进行分析

我们首先对业务场景进行定义,如何快速的从 1000w 数据中找到 10 个数据作为中奖用户,重复100次,每次作为我们终将的名单,切每一个用户不能重复中奖。基于这个过程我们如何把这个过程高效的实现呢?

数据库实现:

使用数据库是最直接的方法,但在处理大规模数据时,性能可能成为瓶颈。

1
2
select id from User where status=0 order by rand() limit 10
update user set status=1 where id in(...)

使用Redis实现:

使用Set数据结构来存储中奖用户,可以提高抽奖效率。具体实现如下:

  • 使用Redis Set: 将中奖用户的ID存储在Redis的Set数据结构中,确保不会有重复的用户。
1
2
sadd usrlist uuid1
sadd usrlist uuid2
  • 抽奖逻辑优化: 通过Redis提供的spop命令,从Set中弹出指定数量的元素,实现高效的抽奖。
阅读全文 »

在缓存使用过程中,通常包含以下三个步骤:

  1. 查询缓存中是否存在数据。
  2. 查询数据库数据。
  3. 将数据写入缓存。

在仅考虑这个过程时,似乎没有问题。然而,当数据发生修改时,我们需要看看可能出现的问题。
修改策略通常有三种:

  1. 先修改数据库,然后删除缓存。
  2. 先删除缓存,然后更新数据库。
  3. 热更新缓存 + 更新数据库。

让我们分别看看这几种场景:

先修改DB 再删除缓存

当两个线程同时进行查询和更新时,存在一个数据更新过程中完成了数据库的修改和缓存的删除的可能性。这样就导致了数据的不一致问题,具体流程如下:

先删除缓存再更新DB

因为没有解决用旧的数据更新缓存的问题,这种方式也存在数据的不一致性的问题。

阅读全文 »

说明:

  1. 此次案例采用的redis是cluster模式。
  2. 网络模型采用 epoll 模式

本篇文章主要讲解 ,从redis原理的角度了解一个 set 命令从redis client发出到 redis server端接收到客户端请求的时候,到底经历了哪些过程?

同样会附带了解下面几个问题

  1. redis的执行原理
  2. Redis cluster集群模式的运行原理
  3. 同样解释了为什么redis的速度
  4. epoll网络模型

为了了解redis请求流程,首先先了解下redis的网络模型。redis 支持 4中网络模式, select、poll、epoll、kqueue ,其中epoll 模型我个人认为是应用最广泛的模型,所以本篇文章以epoll 模型为 demo 进行讲解。

Epoll网络模型

Select 和 poll 模型的缺点:

  1. 每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除,这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,牵涉到用户态到内核态的转移,有一定的开销。
  2. select /poll 返回的值是 int 类型,使得我们不知道是那个 socket 准备就绪了,我们还需要新一轮的系统调用去检查哪一个准备就绪了。

Epoll 模型为了解决 Select ,Poll的两次轮训和每次都需要传入文件描述符的问题,对整体的结构做了一个新的优化,具体架构如下:

阅读全文 »

欢迎继续关注本系列文章,下面我们继续讲解下DDD在实战落地时候,会具体碰到哪些问题,以及解决的方式有哪些。

DDD 是一种思想,主要知道我们方向,具体如何做,需要我们根据业务场景具体问题具体分析。

充血模型和实体Spring注入问题

关于充血模型和Spring注入的问题,你提到了两种解决方案,即使用ApplicationContextAware获取容器中的对象和将依赖作为参数传入。这两种方式都是可行的,但有一些考虑事项:

  1. 使用ApplicationContextAware:
    • 这种方式可以使领域对象(充血模型)直接访问Spring容器中的bean,从而获得依赖的服务。
    • 但要注意,这种方式引入了对Spring容器的强耦合,可能不是DDD的最佳实践,因为领域对象不应该直接依赖于外部容器。
    • 使用这种方式时,要小心管理对象的生命周期和线程安全性,以避免潜在的问题。
  2. 将依赖作为参数传入:
    • 这种方式更符合领域对象的独立性原则,因为它们不依赖于外部容器,而是将依赖作为参数传递进来。
    • 这种方式使得领域对象更容易进行单元测试,因为你可以轻松地传入模拟的依赖对象。
    • 但这可能会导致方法参数列表变得冗长,特别是在领域对象中有多个依赖时。

选择哪种方式取决于具体的需求和项目架构。通常来说,推荐将依赖作为参数传入,因为它更符合领域对象的独立性原则,有助于代码的可测试性和清晰性。但在某些情况下,如果需要在领域对象内部动态获取依赖或与第三方库进行集成,使用ApplicationContextAware也是一个有效的选择。

大聚合根的加载性能问题

大聚合根的加载性能问题是在领域驱动设计 (DDD) 中常见的挑战之一。当一个聚合根包含大量关联实体或值对象,并且需要在应用程序中频繁加载和操作这些关联对象时,可能会导致性能下降。以下是一些解决大聚合根加载性能问题的策略和最佳实践:

  1. 按需加载(Lazy Loading): 采用按需加载的策略,只在需要的时候加载相关对象,而不是一次性加载整个聚合根及其所有关联对象。这可以通过延迟加载技术来实现,确保只有在首次访问关联对象时才加载它们。
  2. 分页加载: 如果可能的话,将大聚合根的关联对象分为多个分页加载,而不是一次性加载所有对象。这可以减轻数据库或持久层的负担,并提高性能。
  3. 缓存: 使用缓存来存储已加载的聚合根和关联对象,以减少数据库查询的次数。缓存可以是内存缓存,如EhCache或Redis,也可以是分布式缓存,具体根据应用程序需求而定。
  4. 事件驱动架构: 在DDD中,可以使用事件驱动架构,当聚合根发生变化时,发布事件通知其他部分。这样,其他部分可以在需要时获取相关数据,而不必依赖于大聚合根的加载。

阅读全文 »

三层架构的问题

在前文中,我从基础代码的角度探讨了如何运用领域驱动设计(DDD)来实现高内聚低耦合的代码。本篇文章将从项目架构的角度,继续探讨三层架构与DDD之间的演化过程,以及DDD如何优化架构的问题。

三层架构作为一种常见的软件架构模式,将应用程序分为展示层、业务逻辑层和数据访问层,具有以下优点:

  1. 分离关注点: 三层架构将不同功能模块分隔开,使每个模块专注于特定任务,降低了代码复杂性。
  2. 可维护性和可扩展性: 不同层之间的松耦合使得对某一层的修改不会影响其他层,有助于系统的维护和扩展。
  3. 可测试性: 不同层的独立性使得单元测试和集成测试更容易实现,有助于确保代码质量。

然而,尽管三层架构有其优点,在处理复杂业务时,三层架构也可能面临一些问题。具体有:

  1. 业务逻辑分散: 在三层架构中,业务逻辑往往分散在不同的层中,导致业务流程难以理清,影响了代码的可读性和可维护性。
  2. 领域模型贫血: 三层架构中,领域逻辑和数据存储混合在一起,导致领域模型的业务方法受限,难以表达复杂的业务规则。
  3. 过度依赖数据存储: 不同层之间对数据存储的依赖紧密,当切换数据存储介质时,需要大量修改代码。

具体具体示意如下图:

image-20230820193649616

随着业务的不断复杂化,service层变得越来越庞大,服务之间的引用也变得越来越混乱,这为项目带来了风险和不确定性。

三层架构演化到DDD

阅读全文 »

在2019年我初次接触到领域驱动设计(Domain-Driven Design,简称DDD)的概念。在我的探索中,我发现许多有关DDD的教程过于偏重于战略设计,充斥着许多晦涩难懂的概念,导致阅读起来相当艰难。有些教程往往只是解释了DDD的概念,而未深入探讨为何要采用这种方式以及这样做能带来哪些好处,这导致很多人在实践应用DDD时遇到了诸多难题。甚至有些人为了引入DDD而在项目中强制采用DDD架构,结果却意外增加了代码的复杂性,带来了一系列潜在的风险。

为了解决这一问题,我计划从代码的基础入手,详细讲解如何将DDD的理念应用于实际开发中,以便解答为何DDD能使我们的代码更加整洁的问题。今天,我们将着重讨论如何运用DDD的思想来组织我们的代码,从而实现”高内聚、低耦合”的开发目标。

在接下来的讨论中,我将与大家分享我在将DDD理念融入实际项目中的一些心得和体会,以及如何在现实项目中充分发挥DDD的优势。无论是战略设计还是战术实施,我都将尽可能以通俗易懂的方式进行解释,希望能够帮助大家更好地理解和应用DDD,从而在编码的道路上越走越远。

首先,让我们看一个电商系统中下单功能的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Autowired
ProductDao productDao;
@Autowired
UserDao userDao;
public void createOrder(String productId,String userId,int count){
Product product = productDao.queryById(productId);
UserInfo user=userDao.queryByUserId(userId);

//风控检测
RiskResponse riskRes= riskClient.queryRisk(xxx);
if(riskRes!=null&&riskRes.getCode()==0&&riskRes.getRiskCode().equal("0001")){
//命中风控
throw new BizException("下单失败,请检查网络")
}

Order order=new Order();
order.setOrderId(IdUtils.generateId());
order.setPrice(product.getPrice()*count);
order.setAddress(user.getAddress());
order.setStatus(OrderEnum.OrderSucess);
orderDao.insert(order);

//预热缓存和增加记录
redisService.set("Order:OrderID_"+order.getOrderId(),order);
orderLogDao.addLog(order);

MessageEntity message = new MessageEntity();
message.setOrderId(order.getOrderId());
message.setMessage("下单成功");
kafkaSender.sent(messageEntity);

}

代码分析

首先,我们对这段代码的逻辑进行整理,共涉及5个步骤:

  1. 查询商品和用户信息
  2. 下单行为的风控检测
  3. 订单创建和持久化
  4. 写入缓存和记录下单日志
  5. 发送订单下单成功消息,通知其他系统

我们从这几个过程入手,根据业务的重要性,我们可以将它们划分为核心业务和非核心业务。显然,下单及其相关操作属于核心代码(步骤1、2、3)。与此相比,写日志、写入缓存以及发送Kafka消息则属于下单过程的非核心业务

核心代码分析

阅读全文 »

发现问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    Semaphore semaphore = new Semaphore(nThread);//定义几个许可
//这里
ExecutorService executorService =new ThreadPoolExecutor(1, nThread,
1000L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1));//创建一个固定的线程池
for (T obj : list) {
try {
semaphore.acquire();
executorService.execute(() -> {
try {
func.accept(obj);
} catch (Exception ex) {
Logger.error("startWithMultiThread 出错!"+ex.getMessage());
} finally {
semaphore.release();
}
});
} catch (InterruptedException e) {
Logger.error("startWithMultiThread 出错!"+e.getMessage());
}
}


public static void main(String[] args) {
ArrayList<Object> objects = Lists.newArrayList();
for (int i = 0; i < 1000; i++) {
objects.add(i);
}
startWithMultiThread(objects,5, obj -> {
Threads.sleep(1000);
System.out.println(obj);
});
}
阅读全文 »

线程池的在 Java并发中使用最多的一种手段,也是性能和易用性相对来说比较均衡的方式,下面我们就一起探索先线程池的原理。

线程池分配线程流程

对于线程池的使用,在这篇文章中就不过多的赘述,首先我们先看下线程池的分配线程的逻辑。

我们知道,在创建线程池的有 7 个核心的参数:

corePoolSize:核心线程数

maximumPoolSize:最大线程数

keepAliveTime:空闲线程存活时间

TimeUnit: 单位

workQueue:阻塞队列

ThreadFactory: 线程工厂

RejectedExecutionHandler: 拒绝策略

在这 7 个参数中,其中我们最重要的几个参数是 corePoolSize,maximumPoolSize,workQueue ,这三个参数来决定线程池主要的线程数和任务队列长度。

具体的流程图如下(图片来自网上,侵删):

image-20220813132419857

阅读全文 »

为什么要考虑学习 DDD架构

在学习 DDD 架构前,一直觉得三层架构结构在业务复杂的场景会带来很多很多的问题,但是一直都处于模糊不清的形态,无法准确的定义。直到学习了DDD 的概念。

为了更好的学习 DDD ,我们总结一下三层架构在业务复杂的场景带来的问题,首先看下正常的项目依赖图

image-20220123220108611

我们正常有 5 个模块,UI(application), Service,Repository,Entity 和 Common,每个层代表的含义,大家都非常清楚,这样会带来什么样的问题呢?

  1. Service 层对 Repository 依赖比较乱,没有明确的规则和界限
  2. 虽然 Repository 对 Enity 是一对一的依赖,但是由于 Service 和 Repository 的依赖的 “混乱”,导致 Entity 的引用带来同样的问题
  3. 每一个层级都对 Common 都有依赖,使得 Common 层从辅助层变成了核心层。
阅读全文 »

为什么引入三色标记法

为了提供 JVM 垃圾回收的性能,从 CMS 垃圾收集器开始,引入了并发标记的概念(此处的并发标记是指与用户线程一起工作)。引入并发标记的过程就会带来一个问题,在业务执行的过程中,会对现有的引用关系链出现改变。具体如下图:

image-20210626215542908

阅读全文 »