目录
Redis之缓存双写
一、双检加锁策略
二、更新策略
1、先更新数据库,再更新缓存
2、先更新缓存,再更新数据库
3、先删除缓存,再更新数据库
4、先更新数据库,再删除缓存
三:canal消息中间件
1、mysql配置
2、canal服务端
3、canal客户端
同步写策略:写数据库后也同步写redis缓存,缓存和数据库中的数据一致;
异步写策略:数据库变动之后,在业务上容许出现一定时间后才作用于redis,比如仓库、物流系统。
多个线程同时去查询数据库的这条数据,可以在第一个查询数据的请求上使用一个互斥锁来锁住
它。其他的线程走到这一步拿不到锁就开始等待,等第一个线程查询到了数据,然后做缓存。后面
的线程进来发现已经有缓存了,就直接走缓存。
public User fandUserById(Integer id){
User user = null;
String key = CACHE_KEY_UESR + id;
//先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
user = (User)redisTemplate.opsForValue().get(key);
if (user == null){
//进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
synchronized (UserService.class){
//3二次查redis还是null,可以去查mysql了(mysql默认有数据)
user = (User)redisTemplate.opsForValue().get(key);
if (user == null){
//4查询mysql拿数据
user = userMapper.selectUserById(key);
if (user == null){
return null;
}else {
//mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
redisTemplate.opsForValue().setIfAbsent(key,user,7L, TimeUnit.DAYS);
}
}
}
}
return user;
}
问题1、mysql更新成功,redis更新失败
问题2、多线程环境下,A、B两个线程有快有慢,有前有后有并行,A先更新数据库,然后B更新数据库,B更新redis,最后A在更新redis,最终结果,mysql和redis数据不一致。
不推荐,业务上一般把mysql作为底单数据库,保证最后解释
问题:多线程环境下,A、B两个线程有快有慢,有前有后有并行,A先更新redis,然后B更新redis,B更新数据库,最后A在更新数据库,最终结果,mysql和redis数据不一致。
问题:多线程环境下,A、B两个线程有快有慢,有前有后有并行,A先删除缓存,然后A更新数据库但由于网络延迟A还没有更新完数据库,B查数据发现缓存中没有,然后去数据库查找,并将结果返回给redis,最后A更新数据库完成,结果数据库是最新数据,redis还是老数据,导致数据不一致。
解决方法:延时双删策略
在先删除缓存,在更新数据库,更新完数据库之后在删除缓存。
问题:假如缓存删除失败或者来不及删除,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。
如何保证数据的最终一致性?使用消息中间件
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4) 另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
问:如果数据库有增删改操作,立刻同步到redis怎么做
使用阿里巴巴的canal消息中间件。
canal地址:https://github.com/alibaba/canal?tab=readme-ov-file
canal:译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅
和消费。
基于日志增量订阅和消费的业务包括
canal 工作原理
canal 使用方法
准备好mysql和redis数据库
1、查看版本
select version();
2、查看MySQL是否启用了二进制日志文件;
SHOW VARIABLES LIKE 'log_bin';
第一次查看为OFF默认没有开启
3、查看当前的主机二进制文件MySQL主节点的状态, 没打开二进制文件夹之前查不到
show master status;
4、开启mysql的bnlog写入功能
打开mysql安装目录找到my.imi文件(先备份),将下面配置放在mysqld下面
#开启binlog
log-bin=mysql-bin
#选择ROW模式
binlog-format=ROW
#配置MySQL replaction需要定义,不要和canal的slaveld重复
server_id=1
5、重启mysql
方式一:打开 Windows 的“服务”窗口:按下 Win + R 组合键打开运行窗口,输入 services.msc
并按回车键。在服务列表中找到 MySQL 服务,然后右键单击该服务并选择“重新启动”。
方式二:打开命令提示符窗口:按下 Win + R 组合键打开运行窗口,输入 cmd
并按回车键。在命令提示符中输入以下命令,以停止 MySQL 服务:
net stop mysql
如果 MySQL 服务停止成功,则输入以下命令来启动 MySQL 服务
net start mysql
6、再次查看MySQL是否启用了二进制日志文件;
SHOW VARIABLES LIKE 'log_bin';
SHOW MASTER STATUS;
返回的结果为当前主节点的二进制日志文件名(File)和偏移量(Position)
7、、授权canal连接mysql账号
mysql默认的用户在mysql库的user表里,查看mysql用户
SELECT* FROM mysql.`user`
#创建一个用户名为 canal,主机名为 %(即允许从任何主机连接)的 MySQL 用户,密码为 canal。
create user 'canal'@'%' identified by 'canal';
#授予该用户在所有数据库上的所有权限。
grant all privileges on *.* to 'canal'@'%' identified by 'canal';
#刷新 MySQL 权限缓存以使更改生效。
flush privileges ;
#查询 mysql.user 表中的用户信息。
SELECT* FROM mysql.user;
1、下载
打开canal官网,这里下载v1.1.6
2、解压
长传到linux系统,创建一个canal文件夹
tar -zxvf canal.deployer-1.1.6.tar.gz
3、配置
修改/mycanal/conf/example路径下的instance.properties文件
vim打开配置文件,换成自己mysql主机的master的IP地址
4、启动
进入bin目录 /root/canal/bin,开启canal(需要先安装jdk1.8)
5、查看
判断canal是否启动成功
方式一:查看server日志,进入/root/canal/logs/canal目录,查看canal.log日志
显示the canal server is running now ..... 说明启动成功
方式二:进入目录/root/canal/logs/example 查看样例example的日志
Redis用RedisTemplate
1、创建一个springboot工程
2、需改pom文件
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.5.14
com.xfcy
canal_demo
0.0.1-SNAPSHOT
canal_demo
canal_demo
UTF-8
1.8
1.8
4.12
1.2.17
1.16.18
5.1.47
1.1.16
4.1.5
1.3.0
com.alibaba.otter
canal.client
1.1.0
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
org.springframework.boot
spring-boot-starter-aop
org.aspectj
aspectjweaver
mysql
mysql-connector-java
5.1.47
com.alibaba
druid-spring-boot-starter
1.1.10
com.alibaba
druid
${druid.version}
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.spring.boot.version}
cn.hutool
hutool-all
5.2.3
junit
junit
${junit.version}
org.springframework.boot
spring-boot-starter-test
test
log4j
log4j
${log4j.version}
org.projectlombok
lombok
${lombok.version}
true
javax.persistence
persistence-api
1.0.2
tk.mybatis
mapper
${mapper.version}
org.springframework.boot
spring-boot-autoconfigure
org.springframework.boot
spring-boot-maven-plugin
3、修改yml文件
server.port=5555
# ========================alibaba.druid=====================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.238.130:3306/bigdata?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.druid.test-while-idle=false
4、RedisUtils类
package com.xfcy.canal_demo.util;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisUtils {
public static final String REDIS_IP_ADDR = "192.168.238.110";
public static final String REDIS_pwd = "111111";
public static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool=new JedisPool(jedisPoolConfig,REDIS_IP_ADDR,6379,10000,REDIS_pwd);
}
public static Jedis getJedis() throws Exception {
if(null!=jedisPool){
return jedisPool.getResource();
}
throw new Exception("Jedispool is not ok");
}
}
RedisCanalClientExample
package com.xfcy.canal_demo.biz;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import com.xfcy.canal_demo.util.RedisUtils;
import redis.clients.jedis.Jedis;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class RedisCanalClientExample {
public static final Integer _60SECONDS = 60;
public static final String REDIS_IP_ADDR = "192.168.238.130";
//增
private static void redisInsert(List columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
}catch (Exception e){
e.printStackTrace();
}
}
}
//删
private static void redisDelete(List columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.del(columns.get(0).getValue());
}catch (Exception e){
e.printStackTrace();
}
}
}
//改
private static void redisUpdate(List columns)
{
JSONObject jsonObject = new JSONObject();
for (Column column : columns)
{
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
jsonObject.put(column.getName(),column.getValue());
}
if(columns.size() > 0)
{
try(Jedis jedis = RedisUtils.getJedis())
{
jedis.set(columns.get(0).getValue(),jsonObject.toJSONString());
System.out.println("---------update after: "+jedis.get(columns.get(0).getValue()));
}catch (Exception e){
e.printStackTrace();
}
}
}
//监视器
public static void printEntry(List entrys)
{
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
//获取变更的row数据
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error,data:" + entry.toString(),e);
}
//获取变动类型
EventType eventType = rowChage.getEventType();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType));
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.INSERT) {
redisInsert(rowData.getAfterColumnsList());
} else if (eventType == EventType.DELETE) {
redisDelete(rowData.getBeforeColumnsList());
} else {//EventType.UPDATE
redisUpdate(rowData.getAfterColumnsList());
}
}
}
}
//启动类
public static void main(String[] args)
{
System.out.println("---------O(∩_∩)O哈哈~ initCanal() main方法-----------");
//=================================
// 创建链接canal服务端
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(REDIS_IP_ADDR,
11111), "example", "", ""); // 这里用户名和密码如果在这写了,会覆盖canal配置文件的账号密码,如果不填从配置文件中读
int batchSize = 1000;
//空闲空转计数器
int emptyCount = 0;
System.out.println("---------------------canal init OK,开始监听mysql变化------");
try {
connector.connect();
//connector.subscribe(".*\\..*");
connector.subscribe("db01.t_user"); // 设置监听哪个表
connector.rollback();
int totalEmptyCount = 10 * _60SECONDS;
while (emptyCount < totalEmptyCount) {
System.out.println("我是canal,每秒一次正在监听:"+ UUID.randomUUID().toString());
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
emptyCount++;
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
} else {
//计数器重新置零
emptyCount = 0;
printEntry(message.getEntries());
}
connector.ack(batchId); // 提交确认
// connector.rollback(batchId); // 处理失败, 回滚数据
}
System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试......");
} finally {
connector.disconnect();
}
}
}
java程序下connectors.subscribe 配置的过滤正则
关闭资源代码简写