首页 文章 精选 留言 我的

精选列表

搜索[ddd],共1226篇文章
优秀的个人博客,低调大师

🚀 Erupt 1.8.4 发布,零前端代码、不生成代码、纯注解动态构建、高效开发DDD领域后台

Erupt 通用后台管理框架 Erupt 是一个低代码全栈类框架,它使用Java 注解动态生成页面以及增、删、改、查、权限控制等功能。 零前端代码、零 CURD、自动建表,仅需一个类文件+简洁的注解配置,快速开发企业级 Admin 管理后台。 提供企业级中后台管理系统的全栈解决方案,大幅压缩研发周期,专注核心业务 本次更新内容 🐞 修复 @RowOperation 权限定义报错的BUG 🌟 EruptDao 新增 JdbcTemplate 调用,更加优雅的执行原生SQL 项目官网:www.erupt.xyz @Erupt( name = "简单的例子", power = @Power(importable = true, export = true) ) @Table(name = "t_simple") //数据库表名 @Entity public class Simple extends BaseModel { @EruptField( views = @View(title = "文本"), edit = @Edit(title = "文本", notNull = true, search = @Search) ) private String input; @EruptField( views = @View(title = "数值", sortable = true), edit = @Edit(title = "数值", search = @Search) ) private Float number; @EruptField( views = @View(title = "布尔"), edit = @Edit(title = "布尔", search = @Search) ) private Boolean bool; @EruptField( views = @View(title = "时间"), edit = @Edit(title = "时间", search = @Search(vague = true)) ) private Date date; @EruptField( views = @View(title = "滑动条"), edit = @Edit(title = "滑动条", type = EditType.SLIDER, search = @Search, sliderType = @SliderType(max = 90, markPoints = {0, 30, 60, 90}, dots = true)) ) private Integer slide; @EruptField( views = @View(title = "下拉选择"), edit = @Edit( search = @Search, title = "下拉选择", type = EditType.CHOICE, choiceType = @ChoiceType(fetchHandler = SqlChoiceFetchHandler.class, fetchHandlerParams = "select id,name from e_upms_menu" ) ) ) private Long choice; } 特性 | Features 自动建表:表结构自动生成,无需手动建表 易于上手:会简单的Spring Boot基础知识即可 使用简单:仅需了解@Erupt与@EruptField两个注解即可上手开发 代码简洁:仅需一个.java文件, template、controller、service、dao 都不需要创建 功能强大:动态条件处理,逻辑删除,LDAP,自定义登录逻辑,RedisSession,操作日志等 多数据源:支持:MySQL、Oracle、SQL Server、PostgreSQL、H2,甚至支持MongoDB 高扩展性:支持自定义数据源实现、自定义登录逻辑、动态权限管理、生命周期函数、自定义 OSS 大量组件:滑动输入、时间选择、一对多、图片上传、代码编辑、自动完成、树、多对多、地图等23类组件 丰富展示:普通文本、二维码、链接、图片、HTML、代码段、iframe、swf等 低侵入性:几乎所有功能都围绕注解而展开,不影响Spring Boot其他功能或三方库库的使用 前后端分离:后端与前端可分开部署 响应式布局:支持PC端手机端等各种规格的设备中使用 自定义页面:支持自定义页面,自定义弹出层,且支持:原生H5 / Freemarker / Thymeleaf等方式渲染 前端零代码:前端布局自动构建,一行前端代码都不用写 无需二次开发:仅需引用 jar 包即可 ! 完全不需要了解Angular / React / Vue / Jquery 而且不需要了解JavaScript / HTML / CSS 甚至不需要了解Spring MVC / Mybatis / SQL 在线体验 | Demo 演示地址:https://www.erupt.xyz/demo 账号密码:guest / guest 支持主流 4 款现代浏览器,以及 Internet Explorer 11+,可直接运行在 Electron 等基于 Web 标准的环境上 演示截图 | Screenshot⛰

优秀的个人博客,低调大师

从 Laravel 的死锁说起:实体+ID生成器+LocalCache+乐观锁+工作单元,DDD 落地的银弹组合

凌晨一点四十,阿杰的群炸了。 DBA 老周在群里点名:「谁在跑订单库?死锁告警连了七条。」 阿杰翻身爬起来开电脑。监控面板上,`Deadlock found when trying to get lock; try restarting transaction` 一屏一屏地刷。 他脑子里只有一个念头:**我明明用了 **`DB::transaction()`** 啊。** 第二天早上,老陈端着咖啡路过,扫了一眼他没关的屏幕。 「别盯着日志了,」老陈说,「你那不是并发量的锅,是你把『读』和『写』关进了同一个事务里。」 小迪端着杯子凑过来:「陈哥,死锁不是 MySQL 的脾气不好吗?跟读有啥关系?」 「MySQL 只是个执行者。」老陈拉开椅子坐下,「你先给它画了个圈,它只能在圈里打转。」 「那圈是谁画的?」 「问得很好。今天就把这个圈拆开看看。」 ## 一、第一笔账:显式 save,难用,而且封不住 阿杰的 Laravel 代码长这样: ```javascript public function changeAddress(int $order_id, array $address) { return DB::transaction(function () use ($order_id, $address) { $order = Order::findOrFail($order_id); $order->address = $address; $order->save(); // ← 手写的 if ($order->need_invoice) { $order->invoice()->create([...]); // ← 又一个手写的 } AuditLog::create(['order_id' => $order_id]); // ← 还有 }); } ``` 看起来没问题。问题有三层。 **第一层,靠人记。** `save()` 的调用点散落在几百个 Service 方法里,漏一个,数据就不落库。这种 bug 最恶心的地方在于——它不报错,单元测试也可能过(内存里的对象确实已经改了),只有换一个进程或者换一个实例去读,才会发现数据少了。 **第二层,原子性靠你自己保证。** 上面那个方法,如果 `$order->invoice()->create()` 抛异常,`$order` 的地址已经改完提交了。一次业务操作,数据库里留下半个结果——除非你把每一处都塞进同一个 `DB::transaction`,而那又回到了第一个问题:靠人记。 **第三层,也是真正封不住的地方**:你没法把它抽进基类。 想用模板方法?外面包一层 `save()`——可你根本不知道业务分支改了几个对象、改了哪几个。想用 `Model::saved` 事件或 Observer?那只是"存完之后发个通知",管不了"存什么"。想用 `$model->saveQuietly()`?它解决的是副作用,不是时机。 根因是——**显式 save 把"持久化的决定权"交给了业务代码**。而"存什么、什么时候存、按什么顺序存",本来就该是框架的事。 小迪举手:「那自动提交是怎么做到的?」 老陈打开另一个项目,`php-vibe-coding-frame`,敲了几行: ```javascript // controller/order.php —— 整个闭包已被 unit_of_work() 自动包裹 if_post('/order/address/*', function ($order_id) { $order = dao('order')->find($order_id); $order->address = input_json('address'); // 没有 save(),没有事务,没有 flush // 闭包返回后,框架自己扫一遍变更,统一落库 }); ``` 「改了对象,就结束了。」老陈说,「工作单元在闭包结束时,自己扫一遍它跟踪过的所有实体,把脏的挑出来,在一个事务里按顺序落库。」 「它怎么知道谁脏了?」 老陈把 `frame/orm_entity.php` 拖出来: ```javascript // 实体自带两套值:structs = 数据库里的原值,attributes = 内存当前值 public $structs = []; // 快照 public $attributes = []; // 现况 final public function just_updated() { return $this->attributes != $this->structs; // 不相等 = 脏了 } final public function just_new() { return self::INIT_VERSION === $this->version; // version=0 就是新建的 } ``` 「加载的时候留一份快照,提交时逐字段比对。`attributes != structs` 就是脏检测,`version = 0` 就是新增。**这就是自动提交**。」 **难用 vs 好用、封不住 vs 封得住,差别就在这里:显式 save 是散在每个业务方法里的手工步骤,工作单元是收敛在框架里的一处策略。业务代码不需要知道数据库的存在,这才是分层。** ## 二、第二笔账:为什么"用事务直连"比工作单元更容易死锁 这才是阿杰那天晚上真正踩的坑。 他有两个业务方法: ```javascript // 逻辑一:查订单,改库存 public function ship(int $order_id) { return DB::transaction(function () use ($order_id) { $order = Order::find($order_id); // 读 A $stock = Stock::where('sku', $order->sku)->first(); $stock->decrement('qty', $order->qty); // 写 B —— 锁住了 // ↓ 后面还要算运费、调支付、发 MQ……锁一直不放 $this->call_payment_gateway($order); Queue::push(new SendSmsJob($order->user_id)); }); } // 逻辑二:查库存,回头改订单状态 public function allocate(string $sku) { return DB::transaction(function () use ($sku) { $stock = Stock::where('sku', $sku)->first(); // 读 B $order = Order::where('sku', $sku)->where('status', 'pending')->first(); $order->status = 'allocated'; $order->save(); // 写 A —— 锁住了 Queue::push(new SendNotifyJob($order->user_id)); }); } ``` 单看每个方法都没问题,并发起来就是灾难: ```javascript T1: 读 A —→ 写 B(锁住 B,事务不结束,锁不放) T2: 读 B —→ 写 A(锁住 A,回头等 B) T1: 等 A …… T2: 等 B …… → 死锁 ``` 老陈在白板上圈了两个词。 「**锁窗口**。T1 从 `decrement()` 那一刻锁住 B 行,一直到方法返回、事务提交,锁才释放。这段时间它在算运费、调支付、发 MQ——**锁的持有时间和你的业务代码长度成正比**。你的方法跑 800ms,锁就压 800ms。两个长事务一交叉,环就成了。」 「那工作单元为什么不会?」 「因为**工作单元里,读不进事务**。」 这句话值得停一下。老陈翻到 `frame/database_mysql.php`: ```javascript // 查询走 read 连接(读写分离) function db_query($sql_template, array $binds = [], $config_key = 'default') { return _mysql_database_closure($config_key, 'read', function ($connection) use (...) { ... }); } // 事务走 write 连接 function db_transaction(closure $action, $config_key = 'default') { db_force_type_write(true); return _mysql_database_closure($config_key, 'write', function ($connection) use ($action) { $began = $connection->beginTransaction(); ... }); } ``` 「看清楚了?**读在 read 连接上,事务开在 write 连接上——两个连接。** 你的 SELECT 从头到尾就没进过那个事务。」 老陈继续往下翻,把工作单元的主循环摊开: ```javascript function unit_of_work(Closure $action) { local_cache_delete_all(); $res = $action(); // ← 业务逻辑:查询、计算、调 RPC,全程无事务 $entities = local_cache_get_all(); $sqls = []; foreach ($entities as $entity) { // ← 只生成 SQL,不执行 if ($entity->just_new()) { $sqls[] = $dao->dump_insert_sql($entity); } elseif ($entity->just_updated()) { $sqls[] = $dao->dump_update_sql($entity); } } if ($sqls) { if (count($sqls) > 1) { db_transaction(function () use ($sqls, $db_config_key) { foreach ($sqls as $sql) { _unit_of_work_write($sql['sql_template'], $sql['binds'], $db_config_key); } }, $db_config_key); } else { // 只有一条写语句?连事务都不开 $sql = reset($sqls); _unit_of_work_write($sql['sql_template'], $sql['binds'], $db_config_key); } } return $res; } ``` 「把 `$action()` 和 `db_transaction(...)` 的位置看清楚。」老陈用笔尖点了两下,「**整个业务方法在事务外面。事务里只有几条 UPDATE。**」 于是 T1 的形状变成了: ```javascript [读 A、读 B、算运费、调支付、发 MQ —— 全程无事务,不持锁] ↓ 最后一刻才开事务 [UPDATE stock ... ; UPDATE orders ... ← 只有 2 条语句,约 3ms] ↓ commit ``` 「锁窗口从几百毫秒压到几毫秒。而且——」 老陈翻到 `orm_entity.php` 的 `dump_update_sql`: ```javascript final public function dump_update_sql($entity) { $binds[':id'] = $entity->id; $binds[':old_version'] = $entity->version; // ← 拿读到的旧版本号做条件 foreach ($this->get_dirty($entity) as $column => $value) { $update[] = "$column = :$column"; $binds[":$column"] = $value; } return [ 'sql_template' => 'update '.$this->table_name .' set '.implode(', ', $update) .' where id = :id and version = :old_version', // ← 乐观锁 'binds' => $binds, ]; } ``` 而执行它的地方,只有六行: ```javascript function _unit_of_work_write($sql_template, array $binds = [], $config_key = 'default') { $row_count = db_write($sql_template, $binds, $config_key); otherwise( $row_count === 1, // 影响行数必须是 1 'data in unit of work is expired', // 否则:你的实体过期了 'exception', UNITOFWORK_DEFAULT_ERROR_CODE ); } ``` 「**乐观锁**。带 `version` 条件更新,影响行数不是 1 就抛异常——**它不排队等锁,所以它不参与等待环**。」 「死锁的四个必要条件里,最后那个『循环等待』根本形不成。因为冲突在这里不是"等",是"失败"。」 小迪盯着那行 `where id = :id and version = :old_version` 看了半天:「所以……死锁不是 MySQL 脾气不好,是我们把锁窗口拉得太长了?」 「对。**数据库锁的是行,工作单元锁的是你的设计。前者会死锁,后者不会。**」 「那要是真撞上了呢?」 「那就重试整个请求。」老陈说,「数据已经是新的了,重读一遍再改——比让两个事务互相瞪着强。」 ## 三、银弹:五个零件,少一个都不成立 「那这套东西到底怎么拼起来的?」阿杰问。 老陈在白板上画了五个方块:**实体 + ID 生成器 + LocalCache + 乐观锁 + 工作单元**。 「这五个是一套的。单拿一个出来,都只是另一个麻烦。」 ### 1. 实体(Entity) 以 ID 定义"我是谁",状态可变、行为内聚。在 `php-vibe-coding-frame` 里,实体基类内置五个系统字段,业务字段只声明自己关心的: ```javascript class order extends entity { public $structs = [ 'user_id' => 0, 'amount' => 0, 'status' => 'pending', ]; public static function create(int $user_id, float $amount): order { $order = parent::init(); $order->user_id = $user_id; $order->amount = $amount; return $order; } } ``` `id` / `version` / `create_time` / `update_time` / `delete_time` 五个字段由基类自动管理。**它是工作单元能"跟踪"的前提**——只有实体才有同一性。值对象没有 ID,你无法说清"这个和那个是不是同一个",也就无从跟踪。 ### 2. ID 生成器 在应用层生成,**不等数据库自增回填**。这个框架用的是 Redis INCR 批量取号: ```javascript // 每次向 Redis 要一段号,本地发完再要下一段,省网络往返 function generate_id($mark = 'idgenter') { ... $step_last_id = cache_increment($mark.'_last_id', $step, 0, 'idgenter'); $now_id = $step_last_id - $step; return $now_id += 1; } // 实体初始化时就拿到 id —— 还没入库 protected static function init() { $static = new static(); $static->attributes = $static->structs; $static->id = self::generate_id(); // ← 入库前就有身份 $static->version = self::INIT_VERSION; // 0,就是新增 $static->create_time = $static->update_time = datetime(); local_cache_set($static); // ← 立刻登记进本地缓存 return $static; } ``` 它解决两个具体问题: - 对象在**入库之前**就有身份 → 工作单元可以拿 ID 当 key 去重、去跟踪 - 内存里可以先建关联:订单先有 ID,明细再挂上去,最后统一落库。不用"先插主表拿回 ID 再插子表" 没有它,工作单元只能处理"已存在"的实体,新增的根本管不了——`version = 0` 这个判断也就无从谈起。 ### 3. LocalCache(请求级身份映射) 同一个请求内,**同一行数据只对应一个对象实例**: ```javascript // 本地缓存 —— 请求级别的 Identity Map,同一请求内相同实体只从数据库加载一次 function _local_cache_key($entity_type, $id) { return $entity_type.'_'.$id; // 类名 + 主键 } function local_cache_get($entity_type, $id) { $cached = _local_cache(); $key = _local_cache_key($entity_type, $id); if (isset($cached[$key])) { return $cached[$key]; // 命中就返回同一个对象 } return; } ``` DAO 每次读数据库前,都先 `local_cache_get` 查一遍;命中就直接返回同一个对象。所以下面这段不会有问题: ```javascript $a = dao('order')->find(1); // 第一次查库 $b = dao('order')->find(1); // 命中 LocalCache,拿到的是同一个对象 $a->status = 'paid'; // $b->status 也一起变了 —— 因为它俩就是同一个对象 ``` 它是工作单元的**地基**。没有它,同一行会被加载成两个副本,工作单元 flush 时不知道该听谁的,后写的静默覆盖先写的——这就是经典的丢更新。 ### 4. 乐观锁 `version` 字段,提交时校验。它是**长读窗口的必然补偿**——正因为读不进事务,你读到的是"某个时刻"的数据,从读到写之间可能有别人改了同一行。传统事务靠行锁挡住,工作单元靠 version 检测。 而且它的价值不只是"防冲突":**它让失败变得明确**。 Laravel 里 `$order->save()` 生成的是 `update orders set ... where id = 1`——无条件覆盖,静默丢更新。框架里生成的是 `... where id = :id and version = :old_version`,影响 0 行直接报「data in unit of work is expired」。 **前者是"我不知道我覆盖了别人",后者是"我知道我该重试"。这才是并发控制该有的语义。** ### 5. 工作单元(Unit of Work) 它是那根**把前四个零件串起来的轴**:登记加载过的实体 → 脏检测 → 提交时按顺序 flush 到一个短事务。 而且在这个框架里,它是**零配置的**——路由闭包在 HTTP 入口处就被自动包住了: ```javascript // public/index.php —— 用 if_verify 钩子把每个路由闭包套进工作单元 if_verify(function ($action, $args) { return unit_of_work(function () use ($action, $args) { $data = call_user_func_array($action, $args); // ← 你的业务闭包在这里面跑 ... }); }); ``` 所以开发者写业务代码时,脑子里根本不需要有"持久化"这三个字: ```javascript if_post('/order/ship/*', function ($order_id) { $order = dao('order')->find($order_id); $order->status = 'shipped'; // 改完就完事 $stock = dao('stock')->find_by_column(['sku' => $order->sku]); $stock->qty = $stock->qty - $order->qty; // 改完就完事 }); // 框架接着做:扫 local_cache → 生成 2 条 UPDATE → 一个短事务里提交 → 校验影响行数 ``` 链条闭合起来是这样: |零件|提供什么|缺了会怎样| | --- | --- | --- | |实体|同一性 + 可跟踪的状态|无从判断"改了什么"| |ID 生成器|入库前的身份|新增实体管不了,`version = 0` 也无从判断| |LocalCache|请求内唯一实例|副本互相覆盖,静默丢更新| |乐观锁|冲突检测 + 明确失败|长读窗口必然丢更新| |工作单元|统一 flush + 短事务|前四个零件各自为战,还得手写 save| 再和 Laravel 的写法并排看一眼,差异就非常直观: |关注点|Laravel 默认|php-vibe-coding-frame| | --- | --- | --- | |何时落库|手动 `save()`,散落在 Service 里|框架在闭包结束时自动 flush| |跨聚合原子性|手动套 `DB::transaction`,靠人记|工作单元天然就是一个事务边界| |事务范围|整个业务方法,含 RPC 和 MQ|只有最后那几条写 SQL| |读操作|和写在同一个事务里|走独立 read 连接,不进事务| |并发冲突|`save()` 无条件覆盖,静默丢更新|`where version = :old` + 影响行数校验| |主键生成|数据库自增,插入后才拿到|Redis 批量取号,入库前就有 ID| |同一行的多个引用|多个对象副本,各自为政|LocalCache 保证请求内唯一实例| **五个一起,才叫银弹;单拿一个出来,都只是"另一个麻烦"。** ## 收尾 那天晚上阿杰翻着框架源码,在工位白板上补了一行: > 读不进事务,写只占毫秒,冲突交给 version —— 死锁的前提,是你自己造出来的。 老陈路过,丢下一句: 「**显式 save 是把持久化的决定权交给业务代码,工作单元是把它收回来交给框架。凡是靠人记的,迟早会忘。**」 如果你现在也在用 `DB::transaction()` 包着整个业务方法,不妨先做一件事:**把读操作挪到事务外面,把写操作攒到最后**。光这一步,告警就会少一半。 想直接看这套实现,代码在这里: ```javascript git clone https://your-git-host/php-vibe-coding-frame.git cd php-vibe-coding-frame sh project/tool/start_development_server.sh php public/cli.php migrate ``` 写一个 `if_post` 闭包,改个实体属性,然后看框架怎么替你收尾——五分钟后你就会明白,「不用 save」不是偷懒,是把决定权放回了它该在的地方。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册