水平迁库:保证数据一致性、多数据源事务处理

背景

为了降低订单系统的复杂度和压力,计划将顺风车相关的业务、内容抽取独立成新的子系统——顺风车系统。摒弃之前的convenient,以hitch命名。

尿点、痛点
  • 新系统都必须采用mysql数据库替换sqlServer数据库,由于业务需要,不能先进行数据迁移再上线,必须上线同时实现平滑水平迁库,做到用户无感知。
  • 如何保证迁库前后的数据一致性?
  • 如何处理多数据源的数据一致性问题?
  • redis新集群与订单系统的redis集群也要做到平滑迁库。
  • 原本依赖顺风车相关的业务操作抽象成远程服务

SqlServert ---> Mysql 数据迁移方案 -- 双写

经过团队的调研、讨论最终定下数据迁移方案:\color{red}{双写法}
大致思路是新系统上线时以及上线后的一段时间继续采取之前的做法:从sqlServer数据库存取数据,上线后 立即打开\color{red}{双写开关}开始数据同步并采取\color{red}{双写}操作,数据同步过程中在对sqlServer进行读写操作时同时对mySql数据库进行同样的一次读写操作。 在数据同步完成之后确保两个数据库的数据完全一致后,关闭双写开关,此后顺风车的一切读写操作全都由mysql数据库完成,至此完成\color{red}{用户无感知不停机迁库}。流程图如下图所示:

image.png

双写开关:代码中采取硬编码的形式, 在zk配置中心配置双写开关,以便灵活的对开/闭 开关不用修改代码。配置信息和关键代码如下:


AEHA%FX}H8AKCM(NX1(28R6.png

摒弃之前用一次去配置中心取一次的操作,改为配置类(单例)初始化时读取到本项目的所有配置中心数据, 通过与字段对应关系 \color{red}{反射}给字段赋值。大大节省了 从配置中心取数据的高频操作所耗费的时间。
当然,如果配置中心的数据被修改,采用配置中心的\color{red}{回调}机制第一时间给字段重新赋值,注意为了保证实例变量在线程之间的可见性,字段用\color{red}{volatile}修饰。

@Data
public final class ConfCenterProperties {
    /** 迁移开关   true: myql  false: sqlServer*/
    @NotNull
    private volatile Boolean transferSwitch;
    

    /** 配置中心数据源名称 与 当前类字段名 对应关系映射 */
    private static final Map<String, String> FIELD_MAP = new HashMap<>();

    // 新增字段需要配置该对应关系
    static {
        FIELD_MAP.put("xxx.xxx.switch", "transferSwitch");
    }

    private static final ConfCenterProperties instance = new ConfCenterProperties();

    private static final Logger LOGGER = LoggerFactory.getLogger(ConfCenterProperties.class);
    
    //实例化时反射赋值
    private ConfCenterProperties() {
        //获取所有的配置数据,遍历匹配实例变量并反射赋值
        Map<String, ClientDataSource> allDataSourceMap = ConfCenterUtil.getAllDataSource();
        for (String key : allDataSourceMap.keySet()) {
            String fieldName = FIELD_MAP.get(key);
            if (fieldName == null) {
                continue;
            }
            try {
                BeanUtils.setProperty(this, fieldName, allDataSourceMap.get(key).getSourceValue());
                Field field = ReflectionUtils.findField(getClass(), fieldName);
                if (field == null) {
                    throw new Exception(fieldName + "不存在,请检查FIELD_MAP配置");
                }
                NotNull notNull = field.getAnnotation(NotNull.class);
                if (notNull != null && field.get(this) == null) {
                    throw new Exception(fieldName + "不可为空,请检查配置中心配置,数据源为" + key);
                }
            } catch (Exception e) {
                throw new IllegalStateException("初始化配置中心数据异常:" + e.getMessage());
            }
        }
    }

//回调方法
public static void onEvent(DataSourceTransport dataSourceTransport) {
     //配置中心数据发生变化  修改实例变量属性值  代码省略……
}


//配置中心监视器  回调
public class ConfCenterListener implements DataChangeListener {

    @Override
    public void call(DataSourceTransport dataSourceTransport) {
        ConfCenterProperties.onEvent(dataSourceTransport);
    }
}

数据同步: 联系架构、DBA进行数据库数据迁移操作,他们有成熟的一套体系,业务人员只需要关注自身业务即可。 通过与DBA同事交流得知 数据迁移是采用xxxx的方式进行,想进一步深入了解的小伙伴可以。点击此链接

如何保证迁库后 的数据一致性: 重点来了~~~由于业务繁忙,如何确保两个数据库数据的一致性是本次需求的重中之重。稍有不慎产生脏数据或数据丢失就会造成严重的线上事故。一大波用户投诉又会接踵而至,事件单又会处理的让人头皮搔更短 ,浑欲不胜簪。话不多说先来双写迁移图压压鲸@#¥%……&*

image.png

热心读者赵二狗提出如下疑问@!#$%^&*(

  • \color{red}{Q:} 数据迁移完成之后,就能够切到新库提供服务了么?
    \color{red}{A:}答案是肯定的,因为前置步骤进行了双写,所以理论上数据迁移完之后,新库与旧库的数据应该完全一致。
  • \color{red}{Q:} 怎么证明数据迁移完成之后数据就完全一致了呢?
    \color{red}{A:}这就得分为以下三种情况分别讨论
    1. 双insert操作:旧库新库都插入了数据,数据一致性没有被破坏
    2.双delete操作:delete的数据属于[min,now]范围内和范围外分别讨论
    3.双update操作:可以认为update操作是一个delete加一个insert操作的复合操作,所以数据仍然是一致的
  • \color{red}{Q:} 上线时开启双写开关,那什么时候关闭双写开关呢?
    \color{red}{A:}当我们通过DBA提供的工具发现两个数据库的数据并无并无差异,完全一致的时候,就可以关闭双写开关。之后的读写操作全部交由mysql处理。当然啦,如果过程中发现问题,我们可以清空新库的数据重新开始同步数据,直至两个数据库的数据完全一致为止
  • \color{red}{Q:} 旧库进行了insert操作插入一条数据,新库同样也要进行一次insert操作,旧库Insert操作是主键递增,那么这个新库Insert操作的id从何而来呢?
    \color{red}{A:}这个不难解决,旧库执行完insert操作之后我们可以拿到insert后的主键id给新库insert操作使用就好了
  • \color{red}{Q:} 如果一个service方法中,既有sqlServer的写操作又有mysql的写操作,假设其中一个操作失败怎么保证另外的操作会跟着回滚呢? 事务的一致性如何保证?
    \color{red}{A:}小老弟,你可算问到点子上了,这个问题一句两句说不清楚,先接着往后看吧。你会得到你想要的答案~~~~


多数据源事务处理

由于项目中需要对sqlServer数据库和mySql数据库进行双写操作,所以不可避免的要配置两个数据源。那么一系列操蛋的问题接踵而来。

事务四大特性
  • \color{green}{原子性}:一个事务中的操作要么全部成功要么全部失败
  • \color{green}{一致性}:在一个事务执行之前和执行之后数据库都必须处于一致性状态。
  • \color{green}{隔离性}:并发的事务是相互隔离的。
  • \color{green}{持久性}:意味着当系统或介质发生故障时,确保已提交事务的更新不能丢失。即一旦一个事务提交,保证它对数据库中数据的改变应该是永久性的,耐得住任何系统故障。持久性通过数据库备份和恢复来保证。
spring事务抽象

在spring当中为我们的数据访问层提供了很多抽象,在这些抽象的帮助下面我们可以非常方便的在不同的框架当中使用一样的方式来进行数据操作, 其中最重要的一个抽象就是\color{green}{事务抽象}

spring的事务抽象: 一致的事务模型
spring提供了一致的事务模型,不管是用JDBC还是Mybatis或者是Hibernate来操作数据库,也不管我们是使用的datasource的事务还是使用JTA的事务,在这个事务抽象里面都能给他们很好的统一在一起。

  • JDBC/Hibernate/myBatis
  • DataSource/JTA
事务抽象的核心接口
//事务抽象核心接口
public interface PlatformTransactionManager {
    //获取事务信息    
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    //提交事务
    void commit(TransactionStatus var1) throws TransactionException;
    //回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;

PlatformTransactionManager

  • DataSourceTransactionManager
  • JtaTransactionManager
  • HibernateTransactionManger

TransactionDefinition
通过TransactionDefinition 可以获取TransactionStatus ,包含了事务的只读状态、回滚状态等等信息。

  • Propagation 传播特性
  • Isolation 隔离性
  • Timeout 超时设置
  • Read-only status 是否只读
public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;
    ……
  
   int getPropagationBehavior();
    int getIsolationLevel();
    int getTimeout();
    boolean isReadOnly();
    @Nullable
    String getName();
事务传播特性
事务传播特性

默认 required
new和 nested的区别:
new 始终启动一个新事务,跟外层的事务没有关联
nested 两个事务有关联,外部事务回滚,内嵌事务也会回滚 保持一致

事务隔离特性
事务隔离特性

默认-1 完全取决于数据库 可以根据实际需求去做设定或者默认 使用数据库的默认的隔离特性

编程式事务

TransactionTemplate
最基本的最简单的方式就是使用TransactionTemplate,当然你也可以说我就是这么傲娇,我就想直接使用JDBC原生的方法,用Connection在里面去做开始事务,提交事务,回滚事务,当然也可以。

  • TrancastionCallBack 有返回值
  • TranscationCallBackWithoutResult 没有返回值
    PlatformTransactionManger
  • 可以传入TransactionDefinition进行定义
    @Autowired
    private TransactionTemplate transactionTemplate;
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public void insert() {
        log.info("before count:{}",getCount());
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                jdbcTemplate.execute("insert into dog(id,nickname) values(1,'二狗')");
                log.info("ing count:{}",getCount());
                //让事务回滚
                transactionStatus.setRollbackOnly();
            }
        });
        log.info("after count:{}",getCount());
    }

事务开启前0条数据,事务过程当中 1条数据,事务回滚后0条数据

声明式事务

利用spring的动态代理在我们的目标方法上加了一层封装(环绕通知),帮助我们进行模版式的事务操作。

  • \color{red}{配置文件} <tx:annotation-driven/> 开始事务功
 <!-- 配置事务注解驱动 -->
   <tx:annotation-driven transaction-manager="transactionManager"/>
  • \color{red}{注解} @EnableTranscationManagement开始事务功能 大势所趋本文介绍注解方法

@Transaction

  • transactionMager 一般是datasoureTransactionManger,如果就一个,就会默认选中这个
  • propagation
  • isolaton
  • timeout
  • readOnly
  • 如何判断回滚 碰到特定的异常类回滚
@Transactional(rollbackFor = Exception.class)
    public void insert1() throws Exception {
        jdbcTemplate.execute("insert into dog(id,nickname) values(1,'二狗')");
        throw new Exception("回滚术");
    }

\color{red}{注意类内部调用 不会调用代理类的方法,因此事务不会生效。}


大多数人存在的误区

有了声明式事务之后,许多人都会认为只要在调用dao的service方法上加上@Transactional就万事大吉了。而事实情况并非如此。先来看一段代码~~

hellow
多数据源事务如何处理?
透过'事务'看本质
拓展大法好!
redis双写迁移

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容