触发器设计的一些思考

Posted 平静的寄居者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了触发器设计的一些思考相关的知识,希望对你有一定的参考价值。

触发器这东西貌似N年(大约90年代)很流行。比如那时候用Oracle Forms做开发的,大量的业务逻辑都放在触发器里。后来好像就少用了,最多的是用来审计,比如在数据库里放个历史记录表,每次原表数据有变化时,就在触发器里,将改动前后的数据复制到历史记录表里,便于以后的审计或者排错。

为什么现在少用触发器了?对这个问题没有深入的研究和认识。好像有人说触发器比较tricky,容易出问题。另外一个问题我想是业务逻辑放在触发器里,加重了数据库服务器的负担。

没想到现在用Salesforce,又是大量的触发器。原因大概是因为Salesforce可以直接在界面上修改数据,相当于直接对数据库操作;另外有多种手段修改数据,除了apex,还可以通过flow和lwc等。这么多输入方式无法控制数据更新时的行为,只能在触发器里定义逻辑。比如asp.net里,只能通过服务端的api来更新数据,所以可以在api里定义数据更新时的逻辑,就不需要用触发器,而直接对数据库操作,就只能在触发器里定义逻辑了。

Salesforce的这个设计不是很好。另外一个问题是有触发器功能的组件太多,除了触发器,还有record triggered flow,workflow rule, process explorer等,容易造成冲突。好像workbench里有个工具可以列出作用于每个对象的所有触发器(包括flow,workflow rule等),这个需要协调以避免冲突。

另外,昨天碰到一个问题,另外一个开发员说他写的代码出错,追踪下来,是我写的触发器报错,从出错信息来看,是权限问题,我的类是with sharing,而他是个community user,权限不够,访问某个SObject时出错。解决方法貌似很简单,改成without sharing或者去掉with sharing(这样权限就从他那里继承过来,而他是从without sharing里调用的),但是不好,因为with sharing是缺省设置,如果以后增加新的触发器,还得修改,而且对我的类来说,without sharing没有必要,从逻辑上来说不合理。

开始的思路是不处理他那种类型的数据,但问题是如何过滤掉?类型这个字段碰巧是个Lookup字段,因为是before insert触发器,只有Id可用,但Id又不能硬编码写死。比如说foo这个字段是判断类型的,要拿到Id,只能先SELECT Id FROM Foo WHERE Name = \'bar\' 问题是community user没有访问Foo对象的权限。

考虑把这句soql放到trigger里,因为trigger缺省是运行在system context的。但是查了一下,说如果触发器调用with sharing的类,那么触发器也相当于有with sharing的限制了,所以行不通。(https://medium.com/elevate-salesforce/things-to-know-about-apex-triggers-part-1-e0ffd10ddd49)但是实际测试了一下,却证明是可以的。所以结论是:刚进入触发器时,默认是without sharing的,如果在触发器中调用了with sharing的类,那么进入类之后就是with sharing了,而不是整个过程都变成without sharing了。

另外一个考虑是,以前一般说不要把程序逻辑放在触发器。但是从这个例子来看,先在触发器里过滤数据也有它的好处。主要是如果触发器有多个类处理的话,如果每个类都只处理一部分类型的数据,有时可以考虑把过滤的代码放在触发器里。当然,这样相当于把业务逻辑放在了两个地方,不太好。但是,除了解决前述的权限问题外,另外一个好处是如果以后增加新的触发器处理类,或者增加新的触发条件,不会和已有的逻辑冲突。还有的好处是在触发器里一次性过滤完毕,相当于一个分发器,这样不需要把整个数据放到不同的类里一遍遍的筛选。

不过,如前所说,触发器容易出错,所以设计上似乎应该尽量避免,如果能保证只有一个途径更新数据,就不要考虑用触发器来做。

Java分布式定时任务场景的思考与设计

文章目录

Java分布式定时任务场景的思考与设计

假设场景:业务触发之后会更新状态,所以我们需要在业务触发一段时间之后监控业务数据状态是否发生了改变,把没有发生改变的数据抽取出来发送邮件给开发人员。

延迟队列DelayQueue

jdk中DelayQueue可以实现上述需求,DelayQueue就是延时队列。

  1. DelayQueue提供了在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素

  2. DelayQueue是一个无界阻塞队列,只有在延迟期满时,才能从中提取元素。

所以要实现这个功能,我们需要在服务启动之后,开启一个线程不断的去阻塞获取延迟到期的数据,然后在判断这个数据的状态有没有发生改变,如果没有改变则需要发送邮件

参考: springBoot之延时队列

缺点:

  1. 占用了一个线程不停空转去获取延迟队列中的数据
  2. 分布式的场景下,每个服务都会有各自的线程去维护各自的延迟队列
  3. 数据量大的场景下,数据积压会比较大,会占用过多的内存,存在OOM的风险
  4. 在数据量大的场景下,可能会发送大量的邮件,邮件速率会过大
  5. 数据存于内存,没有持久化,出现系统宕机的情况下会丢失数据
  6. 需要在业务代码中埋点

MQ 死信队列+TTL实现延迟队列

对于没有直接支持延迟队列的中间件MQ,可以采用另一种方式来实现延迟队列。

对于支持死信队列的中间件MQ,可以采用死信队列+TTL过期时间来实现延迟消费Message的功能。

以下几种方式会把消息推送到死信队列中:

  1. 消息被拒绝
  2. 消息过期
  3. 队列达到最大长度

所以我们可以利用消息过期会把消息移动到死信队列这一特点,来实现延迟队列。

上面图中的例子,消费者不去监听正常的队列这个队列,而是直接监听死信队列队列。那么我们每次发送消息到正常队列队列,由于设置队列消息的TTL,在TTL时间之后,消息进入到死信队列,从而被消费者消费。从而实现了延迟队列。

对于RabbitMQ中间件来说,网上有很多现成的案例可以参考,对于RabbitMQ的实现可以参考如下:

RabbitMQ消息和队列的TTL以及死信队列和延迟队列

RabbitMQ:过期时间TTL和死信队列、延迟队列

而如果使用了Solace中间件的,网上文档比较少,主要还是以官方文档为主,可以参考如下:

How to delay sending out messages in Solace PubSub+

优点:

  1. 解决了分布式的问题,对于延迟队列的维护交给MQ中间件去做
  2. 解决了持久化的问题,及时系统宕机,但是消息依然保留在了MQ中
  3. 像一些电商中超时未支付关闭订单的场景就会是用这种延迟队列的方式,好处就是吞吐量高(相比起定时任务扫表)

缺点:

  1. RabbitMQ对于消息过期的检测:只会检测最近将要被消费的那条消息是否到达了过期时间,不会检测非末端消息是否过期。造成的问题是:非末端消息已经过期了,但是因为末端消息还未过期,非末端消息处于阻塞状态,所以非末端消息不会被检测到已经过期。使业务产生与预期严重不一致的结果。(所以一般采用设置队列过期时间)
  2. 如果采用对队列设置过期时间的话,如果业务中存在不同的过期时间,则每增加一个新的时间需求,就要新增一个队列。(思考:或许能通过同一个过期时间的队列,但是使用一个requeue_count计数器来控制,比如过期时间就是ttl*requeue_count,一旦过期之后判断requeue_count是否为0,如果不是就减1,然后重新投递到正常队列中,这样就能保证正常队列中的过期时间都是一致的,但是最终的过期时间是不一样的,但是缺点就是存在很多次重新投递的动作)
  3. 当数据量大的时候,也就是有很多同时过期消息的时候,可能会出现发送邮件的速率过快的问题。(思考:有些MQ支持批量获取数据的功能: batch-consumers, 可以通过批处理消息来发送邮件)
  4. 需要在业务代码中埋点

参考:

RabbitMQ死信队列在SpringBoot中的使用

基于RabbitMq的实现消息延时发送的优点以及其局限性

MQ 延迟队列

在 RabbitMQ 3.6.x 之前我们一般采用死信队列+TTL过期时间来实现延迟队列,在 RabbitMQ 3.6.x 开始,RabbitMQ 官方提供了延迟队列的插件。

RabbitMQ 官方提供了延迟队列的插件的优点就是解决了之前采用死信队列+TTL过期时间来实现延迟队列的一些缺点。

参考:RabbitMQ 延迟队列,太实用了!

缺点:

  1. 并不是很多MQ中间件支持了延迟队列,网上都是使用RabbitMQ的居多
  2. 需要在业务代码中埋点

对于使用了Solace中间件,似乎也有类似的功能,但没具体实现过,可以参考官网文档:

Delayed Delivery

在官网中提到了,如果你使用了死信队列的方式来实现延迟队列,可以考虑迁移到新的方式:

If you are currently using DMQs to implement a delivery delay, contact Solace Professional Services for guidance on migrating your implementation to use the delivery-delay option instead.
参考:Configuring Delayed Delivery

Schedule Quartz Job

对于定时任务我们可能最常用的就是Schedule Quartz Job,也支持分布式。或者是使用现成的一些分布式定时任务的框架,比如美团分布式定时调度框架XXL-Job

缺点:

  1. 对于某些业务场景下, 比如电商中超时未支付关闭订单的场景,系统的吞吐量不够高

优点:
2. 可指定corn表达式,自由配置
3. 可以抽取一段时间的数据进行批处理,不存在之前延迟队列一条条处理,邮件发送速率过快的问题也能解决
4. 不需要在业务代码中埋点

总结

其实还是具体业务场景具体分析,并没有哪一套设计就是能解决一切问题的"银弹"。当有分布式定时任务的场景,就可以结合实际场景来考虑一下以上的方案。

以上是关于触发器设计的一些思考的主要内容,如果未能解决你的问题,请参考以下文章

Java分布式定时任务场景的思考与设计

商城系统中商品模块数据库设计的一些思考

对支付平台架构设计的一些思考

设计模式总结对常用设计模式的一些思考(未完待续。。。)

关于设计的一些思考

前后端交互-一些关于接口设计的思考