Redis 如何保持和 MySQL 数据一致

Redis 如何保持和 MySQL 数据一致,第1张

redis在启动之后,从数据库加载数据。

读请求:

不要求强一致性的读请求,走redis,要求强一致性的直接从mysql读取

写请求:

数据首先都写到数据库,之后更新redis(先写redis再写mysql,如果写入失败事务回滚会造成redis中存在脏数据)

在并发不高的情况下,读 *** 作优先读取redis,不存在的话就去访问MySQL,并把读到的数据写回Redis中;写 *** 作的话,直接写MySQL,成功后再写入Redis(可以在MySQL端定义CRUD触发器,在触发CRUD *** 作后写数据到Redis,也可以在Redis端解析binlog,再做相应的 *** 作)

在并发高的情况下,读 *** 作和上面一样,写 *** 作是异步写,写入Redis后直接返回,然后定期写入MySQL

1当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。

解决方法:

这种情况应该是先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。

2在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况

解决方法:

遇到这种情况,可以用队列的去解决这个问,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。

这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询 *** 作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。

1、读请求时长阻塞

由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时间内返回,该解决方案最大的风险在于可能数据更新很频繁,导致队列中挤压了大量的更新 *** 作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库,像遇到这种情况,一般要做好足够的压力测试,如果压力过大,需要根据实际情况添加机器。

2、请求并发量过高

这里还是要做好压力测试,多模拟真实场景,并发量在最高的时候QPS多少,扛不住就要多加机器,还有就是做好读写比例是多少

3、多服务实例部署的请求路由

可能这个服务部署了多个实例,那么必须保证说,执行数据更新 *** 作,以及执行缓存更新 *** 作的请求,都通过nginx服务器路由到相同的服务实例上

4、热点商品的路由问题,导致请求的倾斜

某些商品的读请求特别高,全部打到了相同的机器的相同丢列里了,可能造成某台服务器压力过大,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是很大,但是确实有可能某些服务器的负载会高一些。

img

搜索微信号(ID:芋道源码),可以获得各种 Java 源码解析。

并且,回复书籍后,可以领取笔者推荐的各种 Java 从入门到架构的书籍。

在系统根目录有个 FineReport110 文件夹,这个目录下的所有文件都是服务器生成的。

其中有个 /ehcache_disk_store 文件,经常占用非常大的空间,导致根目录很容易就满了,那么怎么修改 /ehcache_disk_store 存放路径呢?

12 实现思路

/ehcache_disk_store 里放的是两种类型的缓存文件,分别是:数据库二级缓存、文件服务器的缓存

*** 作时并不是直接修改 /ehcache_disk_store 的路径,而是分别修改这两种缓存文件的路径。

2 *** 作方法

21 修改数据库二级缓存路径

在工程目录 WEB-INF/config 文件夹下创建一个名为 ehcachedbproperties 的文件。

其中跟数据库二级缓存路径相关的配置见下图框出来的部分,可根据场景实际进行修改。

文件配置项如下:

# 数据库ehcache配置

  

# 是否禁用二级缓存,默认不禁用false

disableCache=false

 

# 堆中最大缓存空间,默认256M

maxBytesLocalHeap=1G

 

# 缓存是否常驻,true时timeToIdleSecond和timeToLiveSecond无效,默认false

eternal=false

 

# 使用后的空闲时间,默认120

timeToIdleSecond=120

 

# 创建后的存活时间,默认120

timeToLiveSecond=120

 

# 缓存过期策略,取值LRU,LFU,FIFO,CLOCK,默认LRU

memoryStoreEvictionPolicy=LRU

 

# 线程检查缓存超时的间隔时间,默认120

diskExpiryThreadIntervalSeconds=120

 

# 持久化类型,取值LOCALTEMPSWAP,DISTRIBUTED,LOCALRESTARTABLE,NONE,默认NONE

persistenceType=LOCALTEMPSWAP

# 指定数据库二级缓存路径

diskStore=C:\\Users\\root\\Desktop\\tmp\\db_cache

 

# 持久化同步写入,默认false

persistenceSynchronousWrites=false

  

# 每隔一段时间清理过期缓存,单位秒,默认60

recycleCacheMemoryInterval=60

 

# 每隔一段时间debug输出缓存使用情况(仅在调试时使用,同时日志级别需要调至DEBUG),单位秒,为0时不开启,默认0

reportCacheStatusInterval=0

22 修改文件服务器缓存路径

在 FineDB 的 fine_conf_entity 表中新增以下两个字段。新增字段时请直接连接到 FineDB 在表里手动修改,不支持使用 FINE_CONF_ENTITY可视化配置插件 配置。

FineDB 数据库类型和连接方法参见文档:FineDB数据库简介

字段 值

ResourceCacheConfigpersistenceStrategy LOCALTEMPSWAP

ResourceCacheManagerConfigdiskStorePath diskStore=C:\\Users\\root\\Desktop\\tmp\\resource_cache

java灰度现网缓存兼容性问题 ,java缓存一致性问题及解决方案:使用缓存,肯定会存在一致性问题;

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容 易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。

一、讨论一致性问题之前,先来看一个更新的 *** 作顺序问题:

先删除缓存,再更新数据库

问题:同时有一个请求 A 进行更新 *** 作,一个请求 B 进行查询 *** 作。可能出现:

(1)请求 A 进行写 *** 作(key = 1 value = 2),先删除缓存 key = 1 value = 1

(2)请求 B 查询发现缓存不存在

(3)请求 B 去数据库查询得到旧值 key = 1 value = 1

(4)请求 B 将旧值写入缓存 key = 1 value = 1

(5)请求 A 将新值写入数据库 key = 1 value = 2

缓存中数据永远都是脏数据

我们比较推荐 *** 作顺序:

先删除缓存,再更新数据库,再删缓存(双删,第二次删可异步延时)

public void write(String key,Object data){

redisdelKey(key);

dbupdateData(data);

Threadsleep(500);

redisdelKey(key);

}

接下来,看一看缓存同步的一些方案,见下图:

1、 数据实时同步更新

更新数据库同时更新缓存,使用缓存工具类和或编码实现。

优点:数据实时同步更新,保持强一致性

缺点:代码耦合,对业务代码有侵入性

2、 数据准实时更新

准一致性,更新数据库后,异步更新缓存,使用观察者模式/发布订阅/MQ 实现;

优点:数据同步有较短延迟 ,与业务解耦

缺点:实现复杂,架构较重

3 、缓存失效机制

弱一致性,基于缓存本身的失效机制

优点:实现简单,无须引入额外逻辑

缺点:有一定延迟,存在缓存击穿/雪崩问题

4、 定时任务更新

最终一致性,采用任务调度框架,按照一定频率更新

优点:不影响正常业务

优点:不保证一致性,依赖定时任务

二、 缓存击穿、缓存雪崩及解决方案

1 、缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于 并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力

瞬间增大,造成过大压力

2 、缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压 力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩

是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

1)单体服务:此时需要对数据库的查询 *** 作,加锁 ---- lock (因考虑到是对同一个参数数值上 一把锁,此处 synchronized 机制无法使用) 加锁的标准流程代码如下:

/

解决缓存雪崩和击穿方案

/

@Service("provincesService")

public class ProvincesServiceImpl3 extends ProvincesServiceImpl implements ProvincesService{

private static final Logger logger = LoggerFactorygetLogger(ProvincesServiceImpl3class);

@Resource

private CacheManager cm;//使用注解缓存

private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的

private static final String CACHE_NAME = "province";

public Provinces detail(String provinceid) {

// 1从缓存中取数据

CacheValueWrapper valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());

}

//2加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁

doLock(provinceid);//32个省,最多只有32把锁,1000个线程

try{//第二个线程进来了

// 一次只有一个线程

//双重校验,不加也没关系,无非是多刷几次库

valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);//第二个线程,能从缓存里拿到值?

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());//第二个线程,这里返回

}

Provinces provinces = superdetail(provinceid);

// 3从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询

if (null != provinces){

cmgetCache(CACHE_NAME)put(provinceid, provinces);

}

return provinces;

}catch(Exception e){

return null;

}finally{

//4解锁

releaseLock(provinceid);

}

}

private void releaseLock(String userCode) {

ReentrantLock oldLock = (ReentrantLock) locksget(userCode);

//查询锁是否存在和查询当前线程是否保持此锁

if(oldLock !=null && oldLockisHeldByCurrentThread()){

oldLockunlock();

}

}

private void doLock(String lockcode) {//给一个搜索条件,对应一个锁

//provinceid有不同的值,参数多样化

//provinceid相同的,加一个锁,---- 不是同一个key,不能用同一个锁

ReentrantLock newLock = new ReentrantLock();//创建一个锁

Lock oldLock = locksputIfAbsent(lockcode, newLock);//若已存在,则newLock直接丢弃

if(oldLock == null){

newLocklock();//首次加锁,成功取锁,执行

}else{

oldLocklock();//阻塞式等待取锁

}

}

}

2} 集群或微服务场景下:

此场景下的锁换成分布式锁(redis或zk等);同时设置多次取锁功能;

/

解决缓存雪崩和击穿方案

/

@Service("provincesService")

public class ProvincesServiceImpl5 extends ProvincesServiceImpl implements ProvincesService{

private static final Logger logger = LoggerFactorygetLogger(ProvincesServiceImpl3class);

@Resource

private CacheManager cm;//使用注解缓存

@Autowired

private RedisTemplate<String, Object> redisTemplate;

private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的

private static final String CACHE_NAME = "province";

public Provinces detail(String provinceid) throws Exception{

// 1从缓存中取数据

CacheValueWrapper valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());

}

//2加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁

//32个省,最多只有32把锁,1000个线程

boolean flag=false;

flag = RedisUtilsetNX(provinceid, 3000);

//如果首次没有取到锁,可以取10次

if(!flag){

for(int i=0;i<10;i++){

Threadsleep(200);

flag = RedisUtilsetNX(provinceid, 3000);//分布式锁

if(flag){

break;

}

}

}

//如果首次没有取到锁,一直取直到取到为止

/ if(!flag){

for (;;){

Threadsleep(200);

flag = RedisUtilsetNX(provinceid, 3000);//分布式锁

if(flag){

break;

}

}

}/

try{//第二个线程进来了

// 一次只有一个线程

//双重校验,不加也没关系,无非是多刷几次库

valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);//第二个线程,能从缓存里拿到值?

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());//第二个线程,这里返回

}

Provinces provinces = superdetail(provinceid);

// 3从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询

if (null != provinces){

cmgetCache(CACHE_NAME)put(provinceid, provinces);

}

return provinces;

}catch(Exception e){

return null;

}finally{

//4解锁

RedisUtilreleaseLock(provinceid);

}

}

}

这里加分布式锁解决缓存一致性问题,也解决缓存击穿的问题;分布式锁参考:分布式锁使用及原理。

今天缓存一致性问题到此结束,下篇我们使用布隆过滤器解决缓存穿透问题,敬请期待。

一、数据缓存

这里所说的数据缓存是指数据库查询缓存,每次访问页面的时候,都会先检测相应的缓存数据是否存在,如果不存在,就连接数据库,得到数据,并把查询结果序列化后保存到文件中,以后同样的查询结果就直接从缓存表或文件中获得。

用的最广的例子看Discuz的搜索功能,把结果ID缓存到一个表中,下次搜索相同关键字时先搜索缓存表。

举个常用的方法,多表关联的时候,把附表中的内容生成数组保存到主表的一个字段中,需要的时候数组分解一下,这样的好处是只读一个表,坏处就是两个数据同步会多不少步骤,数据库永远是瓶颈,用硬盘换速度,是这个的关键点。

二、页面缓存

每次访问页面的时候,都会先检测相应的缓存页面文件是否存在,如果不存在,就连接数据库,得到数据,显示页面并同时生成缓存页面文件,这样下次访问的时候页面文件就发挥作用了。(模板引擎和网上常见的一些缓存类通常有此功能)。

三、时间触发缓存

检查文件是否存在并且时间戳小于设置的过期时间,如果文件修改的时间戳比当前时间戳减去过期时间戳大,那么就用缓存,否则更新缓存。

四、内容触发缓存

当插入数据或更新数据时,强制更新缓存。

五、静态缓存

这里所说的静态缓存是指静态化,直接生成HTML或XML等文本文件,有更新的时候重生成一次,适合于不太变化的页面,这就不说了。

以上内容是代码级的解决方案,我直接CP别的框架,也懒得改,内容都差不多,很容易就做到,而且会几种方式一起用,但下面的内容是服务器端的缓存方案,非代码级的,要有多方的合作才能做到。

六、内存缓存

Memcached是高性能的,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度。

session会在下面的时间点清理缓存:

(1)当应用程序调用orghibernateTransaction的commit()方法的时候 commit()方法先清理缓存,然后再向数据库提交事务。

(2) 当应用程序执行一些查询 *** 作时,如果缓存中持久化对象的属性已经发生变化,就会先清理缓存,使得Session缓存与数据库进行了同步,从而保证查询结果返回的是正确的数据。

(3) 当应用程序显式调用Session的flush()方法的时候

以上就是关于Redis 如何保持和 MySQL 数据一致全部的内容,包括:Redis 如何保持和 MySQL 数据一致、encache.index文件设置存放路径、java灰度现网缓存兼容性问题等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/sjk/10189912.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-06
下一篇 2023-05-06

发表评论

登录后才能评论

评论列表(0条)

保存