异步系统和多线程开发sop

Posted by 亚雄的博客 on September 25, 2025

异步消息设计SOP

  1. 尽量保证消息来的时候是有时间顺序被系统接收的
  2. 如果系统本身是并行处理,那么在前一条相关的消息没有处理好的时候,需要有检测 条件可以知道,检测到之后把当前消息全部放入队列存起来,之后再按时间排序并处理。
  3. 每条消息被打上唯一的ID,并且保证幂等

幂等就是多次重复执行,但是结果不会破坏业务逻辑。

例如:存数据(例如客户的订单、支付)的时候 (1) 把数据打上唯一的ID标记 之后存的时候判断,如果存过了,那么就不存了。

(2) 使用数据库的约束机制

建立键约束,如果检测到,那么就不重复写入数据。

(3)使用状态机

对于管理的数据,只有几种状态,例如open,paid,close,failed几种状态。

(4)乐观锁

例如把读取和更新作为一对操作,锁在一起,每次只有一个线程能拿到这个锁并做完读取和更新的操作。

具体实现细节:读取的时候,也要读取数据的版本(也就是锁),然后,只能在这个版本的情况下进行更新, 更新完之后立马递增版本号。

坏处:需要重试机制,因为更新操作可能会失败;ß

多个线程抢锁的时候,如果一个线程成功,意味着其他线程都要失败,而失败是要重试的,容易混乱。

结论:适合短事务。

Rails validator线程安全问题

线程不安全的结果也就是validator很可能会出错。

  1. 被validator的对象本身可变,那线程不安全

对一个类C,配置一个validator类VC,而且假设rails每次都保证在为每一个C对象创建一个新的VC对象。

假设类VC里面定义def validate(c),且类C对象存在内存堆里面。

例如类C是房子,一个房子里面会住人,VC负责检测这个房子里面不能低于2个人。

那么,在c对象本身可以被VC1和VC2访问的情况下,假如VC1和VC2同时访问c对象,

VC1稍微快一点,这个时候c里面只有一个人,所以VC1检测返回失败;

VC2稍后,但这个时候房子c里面来了一个人(例如被另一个线程写操作修改),这个时候,VC2的检测返回成功。

这样,系统报出VC1的检测返回失败,这样会对整个系统造成困扰。

  1. 被validator的对象本身不变,但被validator的对象在validate的时候被替换掉了(也就是线程没有自己独立的副本),线程不安全

想象一下这种情形:

(1)假设类VC里面定义了:attr_reader :c

(2)执行各种具体验证方法的时候,里面调用@c

(3)执行一个具体验证方法的时候,VC1的@c被另一个线程VC2替换掉了,从c1变成了c2,导致验证失败

  1. 被validator的对象本身可变,但保证validator线程有自己的独立副本

这就是rails validator的最佳实践:在不能保证c不变的时候,还要尽量线程安全,那么只能从validator对象进行改造,保证 validator线程有自己的独立副本。

首先,rails官方文档里面说validator的实例变量只会被初始化一次,the validator will be initialized only once for the whole application life cycle, and not on each validation run, so be careful about using instance variables inside it.

也就意味着validator的实例是一种类单例,会被rails缓存下来使用。

这么理解:

Rails 内部的工作方式(简化版)

class ActiveModel::Validations::ClassMethods def validates_with(*validators) validators.each do |validator_class|

Rails 为每个验证器类创建一个实例,并缓存它

validator_instance = validator_instances[validator_class] ||= validator_class.new

这个实例会被重复使用

end

优点是: 内存效率:避免为每次验证创建新实例 性能优化:初始化只发生一次 简单的 API:开发者不需要关心实例管理

缺点:线程容易不安全, 例如,如果使用attr_reader :c,@c这种堆对象,那么在验证不同的c的的时候,c2会覆盖之前的c1,也就是覆盖掉@c,导致之前的验证中途因为@c的变化而可能失败。

稍微的线程安全:保证validator线程有自己的独立副本,如何保证呢?不使用堆对象,而改用栈对象,也就是在执行所有具体验证方法的时候,不是使用@c,而是显示的传入c作为参数,

作为参数后,c是存在栈(线程栈)上面的,每个线程有自己的副本。