异步消息设计SOP
- 尽量保证消息来的时候是有时间顺序被系统接收的
- 如果系统本身是并行处理,那么在前一条相关的消息没有处理好的时候,需要有检测 条件可以知道,检测到之后把当前消息全部放入队列存起来,之后再按时间排序并处理。
- 每条消息被打上唯一的ID,并且保证幂等
幂等就是多次重复执行,但是结果不会破坏业务逻辑。
例如:存数据(例如客户的订单、支付)的时候 (1) 把数据打上唯一的ID标记 之后存的时候判断,如果存过了,那么就不存了。
(2) 使用数据库的约束机制
建立键约束,如果检测到,那么就不重复写入数据。
(3)使用状态机
对于管理的数据,只有几种状态,例如open,paid,close,failed几种状态。
(4)乐观锁
例如把读取和更新作为一对操作,锁在一起,每次只有一个线程能拿到这个锁并做完读取和更新的操作。
具体实现细节:读取的时候,也要读取数据的版本(也就是锁),然后,只能在这个版本的情况下进行更新, 更新完之后立马递增版本号。
坏处:需要重试机制,因为更新操作可能会失败;ß
多个线程抢锁的时候,如果一个线程成功,意味着其他线程都要失败,而失败是要重试的,容易混乱。
结论:适合短事务。
Rails validator线程安全问题
线程不安全的结果也就是validator很可能会出错。
- 被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的检测返回失败,这样会对整个系统造成困扰。
- 被validator的对象本身不变,但被validator的对象在validate的时候被替换掉了(也就是线程没有自己独立的副本),线程不安全
想象一下这种情形:
(1)假设类VC里面定义了:attr_reader :c
(2)执行各种具体验证方法的时候,里面调用@c
(3)执行一个具体验证方法的时候,VC1的@c被另一个线程VC2替换掉了,从c1变成了c2,导致验证失败
- 被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是存在栈(线程栈)上面的,每个线程有自己的副本。