背景
为了降低订单系统的复杂度和压力,计划将顺风车相关的业务、内容抽取独立成新的子系统——顺风车系统。摒弃之前的convenient,以hitch命名。
尿点、痛点
- 新系统都必须采用mysql数据库替换sqlServer数据库,由于业务需要,不能先进行数据迁移再上线,必须上线同时实现平滑水平迁库,做到用户无感知。
- 如何保证迁库前后的数据一致性?
- 如何处理多数据源的数据一致性问题?
- redis新集群与订单系统的redis集群也要做到平滑迁库。
- 原本依赖顺风车相关的业务操作抽象成远程服务
SqlServert ---> Mysql 数据迁移方案 -- 双写
经过团队的调研、讨论最终定下数据迁移方案:
大致思路是新系统上线时以及上线后的一段时间继续采取之前的做法:从sqlServer数据库存取数据,上线后 立即打开开始数据同步并采取操作,数据同步过程中在对sqlServer进行读写操作时同时对mySql数据库进行同样的一次读写操作。 在数据同步完成之后确保两个数据库的数据完全一致后,关闭双写开关,此后顺风车的一切读写操作全都由mysql数据库完成,至此完成。流程图如下图所示:
双写开关:代码中采取硬编码的形式, 在zk配置中心配置双写开关,以便灵活的对开/闭 开关不用修改代码。配置信息和关键代码如下:
摒弃之前用一次去配置中心取一次的操作,改为配置类(单例)初始化时读取到本项目的所有配置中心数据, 通过与字段对应关系 给字段赋值。大大节省了 从配置中心取数据的高频操作所耗费的时间。
当然,如果配置中心的数据被修改,采用配置中心的机制第一时间给字段重新赋值,注意为了保证实例变量在线程之间的可见性,字段用修饰。
@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的方式进行,想进一步深入了解的小伙伴可以。点击此链接
如何保证迁库后 的数据一致性:
重点来了~~~由于业务繁忙,如何确保两个数据库数据的一致性是本次需求的重中之重。稍有不慎产生脏数据或数据丢失就会造成严重的线上事故。一大波用户投诉又会接踵而至,事件单又会处理的让人头皮搔更短 ,浑欲不胜簪。话不多说先来双写迁移图压压鲸@#¥%……&*
热心读者赵二狗提出如下疑问@!#$%^&*(
- 数据迁移完成之后,就能够切到新库提供服务了么?
答案是肯定的,因为前置步骤进行了双写,所以理论上数据迁移完之后,新库与旧库的数据应该完全一致。
- 怎么证明数据迁移完成之后数据就完全一致了呢?
这就得分为以下三种情况分别讨论
1.
双insert操作:旧库新库都插入了数据,数据一致性没有被破坏
2.
双delete操作:delete的数据属于[min,now]
范围内和范围外分别讨论
3.
双update操作:可以认为update操作是一个delete加一个insert操作的复合操作,所以数据仍然是一致的
- 上线时开启双写开关,那什么时候关闭双写开关呢?
当我们通过DBA提供的工具发现两个数据库的数据并无并无差异,完全一致的时候,就可以关闭双写开关。之后的读写操作全部交由mysql处理。当然啦,如果过程中发现问题,我们可以清空新库的数据重新开始同步数据,直至两个数据库的数据完全一致为止
- 旧库进行了insert操作插入一条数据,新库同样也要进行一次insert操作,旧库Insert操作是主键递增,那么这个新库Insert操作的id从何而来呢?
这个不难解决,旧库执行完insert操作之后我们可以拿到insert后的主键id给新库insert操作使用就好了
- 如果一个service方法中,既有sqlServer的写操作又有mysql的写操作,假设其中一个操作失败怎么保证另外的操作会跟着回滚呢? 事务的一致性如何保证?
小老弟,你可算问到点子上了,这个问题一句两句说不清楚,先接着往后看吧。你会得到你想要的答案~~~~
多数据源事务处理
由于项目中需要对sqlServer数据库和mySql数据库进行双写操作,所以不可避免的要配置两个数据源。那么一系列操蛋的问题接踵而来。
事务四大特性
- :一个事务中的操作要么全部成功要么全部失败
- :在一个事务执行之前和执行之后数据库都必须处于一致性状态。
- :并发的事务是相互隔离的。
- :意味着当系统或介质发生故障时,确保已提交事务的更新不能丢失。即一旦一个事务提交,保证它对数据库中数据的改变应该是永久性的,耐得住任何系统故障。持久性通过数据库备份和恢复来保证。
spring事务抽象
在spring当中为我们的数据访问层提供了很多抽象,在这些抽象的帮助下面我们可以非常方便的在不同的框架当中使用一样的方式来进行数据操作, 其中最重要的一个抽象就是。
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的动态代理在我们的目标方法上加了一层封装(环绕通知),帮助我们进行模版式的事务操作。
- <tx:annotation-driven/> 开始事务功
<!-- 配置事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
@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("回滚术");
}
大多数人存在的误区
有了声明式事务之后,许多人都会认为只要在调用dao的service方法上加上
@Transactional
就万事大吉了。而事实情况并非如此。先来看一段代码~~
hellow