关于高并发的思考
引言
每次出现奇怪的问题,怎么都查不出来的时候,初步看代码也没什么问题,我大概率怀疑是并发导致的。在某一步因为并发导致数据缺失或者被覆盖。
在开发中比较常见的就是资源的查询和更新。如果查询和更新接口并发访问,那么在执行的时候就会有2种可能性。查询在更新前,那么返回给客户端的是旧的数据,服务器后续更新接口返回成功,客户端用哪个就不好说了。查询在更新后,这个是符合预期的结果。
如果是2个更新相关的接口同时并发,那么就更糟糕了。如果业务复杂的话,更新的逻辑相互嵌套,这会就更难以预测到底会发生什么。
多个并发那是噩梦了。
思考
首先是确定程序是最终一致性还是强一致性以及幂等性,要是客户端要是再次触发,怎么避免重复。
一致性问题
解决方法最先想到的就是加锁来确保这一点,但是在业务复杂的场景下很难做,锁多了的话,一个处理不好就可能导致死锁的问题,且性能影响也是比较巨大,想做好很难。全局锁能解决一部分问题。但是十分影响性能。
实际中并发主要问题就是一些公共资源的同时使用。确保修改的时候值是没有被其它修改过的。
主流的主要是乐观和悲观的实现。悲观确保执行的时候只有一个线程涉及公共资源的在运行。乐观是在最后修改的时候在进行对比旧值进行修改。
幂等性即可重复性
一个操作应该是可以重复的,即无论做多少次,产生的结果是一样的。换句话来说就是接口如果参数不变的话,调用多少次都是一样的结果,不会产生别的影响。
举例
在缓存的时候,更新数据库的值和缓存的值。如果先更新数据库,在更新缓存。那么在这期间就存在缓存没有及时更新的情况(部分场景是很致命的)。一种先更新缓存,在更新数据库,这种存在的问题是数据库更新失败的时候,不好处理。还有是先删除缓存,在更新数据库。但这里存在问题是,可能别的线程在删除缓存后又读出数据,导致缓存存储旧值。在优化一点是先删除缓存,更新数据库后,间隔时间在删除一次缓存(时间大于读数据库+写缓存)。这个方案的漏洞是如果延迟删除缓存失败,还是可能存在存储旧值的情况。这里面需要机制来确保,重试机制或者消息机制。重要一点是更新数据库的时候最好加上旧值的判断(乐观锁的概念)。
该例子目标是为了最终一致性。这适用于中间状态不对也不会产生持久化的负面影响的场景。
从点到面,确保底层数据的一致性和操作幂等性,模块的一致性其实也类似,需要考虑失败的情况,比如更新第一个数值成功了,更新第二个数值失败,因为解耦合,没办法简单用事务等方式来处理(且 mysql 事务并不难阻止常见的并发更新,除非用 for update 悲观锁或者手动实现乐观锁)。这种没有通用的做法,因为你没办法要求每次提供更新数据的接口,还要提供回滚的接口。只能尽量从设计层面避免,将影响降到最低。比如失败的情况,增加重试或者标记或者监控等等方式结合来实现程序的稳定。
结论
高并发要想完全解决是一件基本不可能的事情,不可能为了千分之一甚至万分之一的概率来投入大量的成本。在设计层面进行把关是最优解。当然底层数据存储的一致性是肯定要确保的。