原创文章,禁止转载
一、一致性问题的引入
为了提高性能,很多设计都引入了中间层,都需要考虑一致性问题,比如以下几种在不同领域或层面的设计:
在高速缓存和内存之间: CPU 和内存访问性能的差距非常大,为了弥补两者之间的性能差异,充分利用 CPU,现代 CPU 中引入了高速缓存(CPU Cache);高速缓存会把内存中的数据读取到高速缓存,这时CPU从高速缓存读取速度就很快,当然这是非常底层的内容,他们的一致性处理的办法是我们应用层软件开发人员无需关心的问题。
在工作内存和主内存之间: 线程运行的代码对应的是一些指令,是由CPU执行的!但是CPU每次执行指令运算的时候,也就是执行我们写的代码的时候,要是每次需要一个变量的值,都从主内存加载,性能会比较差!为了提高读取主内存性能,引入了工作内存;通常为了一致性,我们会加入锁或者volitile等。
Redis和数据库之间: 为了解决数据库瓶颈,我们引入了Redis缓存,Redis先把数据库的数据读取到Redis中,应用就从Redis读取数据,而不是直接从数据库读取。
在微服务之间: 因为是跨库操作,不能像单库那样保证ACID那么简单,需要单独去实现分布式事务的功能,这里通常大家听说过的异步确保型、TCC等。
以上各种,除了微服务之间不一定是为了性能所以增加了中间层或者服务,其他都是为了性能所以增加了中间层,而这里提高性能并不是直接提高本身,比如第二张图为了提高访问数据库的速度,这里并不是直接提高了数据库的性能,而是加了缓存,把数据库的数据放到缓存,各个应用不直接访问数据库,而是改成访问缓存,但是这样就需要保证缓存和数据库数据的一致性问题,当数据库数据变更了,就要及时放到缓存。
对于我们应用软件开发人员,除了高速缓存和内存之间这种硬件层面的一致性不需要我们关心外,其他情况都需要我们去注意的。
** **
二、在工作内存和主内存之间
我们来大概看看工作内存与主内存的关系图,比如我们有2个线程同时从主内存获取数据,之后把数据放到各自工作内存:
这时,线程1进行减1操作,线程1的工作内存数据变成了0,还没把数据放到主内存,
但这时线程2是看不到线程1的数据的,此时线程2也进行减1操作,data也是0,他们俩线程都往内存里面写入数据,结果等于0,但2个线程都减了1,应该结果是1-1-1=-1的。
为了解决这个问题,我们可以在data变量上面加volitile修饰符,它可以及时把修改后的工作内存中的数据放到主内存,且让其他线程中的工作内存中的该变量过期从而重新从主内存获取数据。\
\
二、微服务一致性
微服务之间,通常比如一个大型系统,我们拆分成了财务服务、用户服务、支付服务,订单服务,这几个服务都是各自独立数据库的,当我们有一个业务操作,比如订单的支付,是从订单服务发起的,我们可以保证订单本地服务数据库数据的一致性,但是其他几个服务比如支付服务的数据、财务服务的数据就不一定了,
就像如上图,用户下单,订单服务处理数据库成功,调用支付服务返回也是成功的,但调用库存服务失败,那订单服务、库存服务、支付服务的数据是不一致的。
该这情况的处理方式相对来说是比较复杂的,大家可以看看我其他多篇文章关于分布式事务的讲解,或者进入我的公众号获取文章合集浏览文章目录。
二、Redis和数据库一致性
这里也只是说说比较简单常见的处理方式,看下图,通常我们读取数据的时候,是下面的流程,我们先从缓存里面读取数据,如果没有数据,就从数据库读取数据放到缓存中。
但后来,应用2进行了数据库减1操作,把data变成了0
这时,应用1和应用2因为都是先从缓存读取的,看到缓存有数据,且data还是等于1,这时就出现了Redis缓存和数据库不一致的情况。
该如何解决这样的问题:
1-当具体的业务数据对实时性要求不高,可以允许有延迟或者有不准确的情况,那我们可以定时把数据库的数据定期覆盖到缓存中去。
2-当我们业务数据对实时性要求很高时,当我们修改了数据库后,立马将Redis缓存的数据过期掉,但是这里其实还细分准实时性和完全实时性问题
1)准实时性就是还是允许一点点的延迟,那可以用我们上面说的方法
2)而完全实时性,这里其实还是有一点相对复杂的,比如虽然我们是先修改数据库,后过期缓存,但这2者操作之间可能还是有其他的并发操作过来导致这段时间读取缓存还是旧数据,而当我们先过期缓存数据,再去修改数据库,但这样的操作可能会导致缓存的数据库更新成功了,但数据库的数据更新失败。所以这种情况需要另外相对复杂点的处理,因为这篇文章只是简单的引入一致性问题,所以这里不做过多的说明。
更多分享请关注我的公众号