知名网站欣赏,网站用户管理体系,公司网站免费自建,wordpress wp-config背景
考试系统中#xff0c;教师会在后台发布一场考试#xff0c;考试会存储在MySQL和Redis里面#xff0c;考试有时候是会出错的#xff0c;我们需要后台修改#xff0c;如果多个教师在后台并发修改#xff08;概率不大#xff09;#xff0c;可能会出现数据库缓存不…背景
考试系统中教师会在后台发布一场考试考试会存储在MySQL和Redis里面考试有时候是会出错的我们需要后台修改如果多个教师在后台并发修改概率不大可能会出现数据库缓存不一致的问题我们需要解决一下刚好联系一下这一块的知识
基础知识理论
四种解决办法
先更新数据库再更新缓存先更新缓存后更新数据库先删除缓存后更新数据库先更新数据库后删除缓存
这四种情况都会出现缓存不一致的问题但是第四种出现缓存不一致的情况概率上比较低因为缓存的写入回远远快于数据库的写入我们可以使用【先更新数据库再更新缓存】的方式来解决数据库不一致的问题为了确保万无一失我们还可以加上一个过期时间。
但是有两个问题
我们需要保证先更新数据库再更新缓存是一个原子操作不然如果更新缓存失败了导致缓存中的数据还是旧值「先更新数据库再删除缓存」的方案虽然保证了数据库与缓存的数据一致性但是每次更新数据的时候缓存的数据都会被删除这样会对缓存的命中率带来影响。
对于第一个问题解决办法是消息队列重试机制、订阅MySQL binlog再操作缓存
对于第二个问题是我们使用「更新数据库 更新缓存」的方式来实现但是这个方案本身固有一些问题解决办法是分布式锁和过期时间
另外先删除缓存再更新数据库的解决方案是延迟双删
实战-先更新数据库后删除缓存
此种方案存在问题是两个操作不是原子性的如何保证两个操作都能成功呢 方案一gin框架消息队列
我们实现一个中间件
func Canal() gin.HandlerFunc {//封装成一个中间件当我们更新考试的时候会自动监测到return func(c *gin.Context) {c.Next()redisDB : global.GVA_REDISget, exists : c.Get(DeleteRedisKey)DeleteKey : get.(string)if exists {err : redisDB.Del(context.Background(), DeleteKey).Err()if err redis.Nil {//无需删除c.Next()} else if err ! nil {//删除失败加入到redis异步队列中再次删除addToDelayedQueue(context.Background(), redisDB, DeleteKey, time.Now().Add(10*time.Second))}else {global.GVA_LOG.Info(更新数据成功)}} else {zap.L().Info(redis中不存在该键无需删除)}}
}
我们在继承这个中间件的接口中的使用c.Set方法传递要删除的redisKey然后我们在上述中间件中使用c.Get方法来接收要删除的redisKey然后加入到异步队列中删除
现在来模拟一下是否能够成功项目启动之后把redis停机然后删除redis的key就会失败然后该key就会加入延时队列 方案一消息队列订阅binlog 里面有两个难点一个是订阅binlog服务一个是消息队列 如何订阅binlog服务我们使用阿里巴巴的canal 支持Go语言具体参考Golang通过alibabaCanal订阅MySQLbinlog_大杯无糖的博客-CSDN博客 接下来是用redis的zset实现一个异步消息队列