背景
為了降低訂單系統(tǒng)的復(fù)雜度和壓力豆茫,計(jì)劃將順風(fēng)車相關(guān)的業(yè)務(wù)侨歉、內(nèi)容抽取獨(dú)立成新的子系統(tǒng)——順風(fēng)車系統(tǒng)。摒棄之前的convenient澜薄,以hitch命名为肮。
尿點(diǎn)、痛點(diǎn)
- 新系統(tǒng)都必須采用mysql數(shù)據(jù)庫替換sqlServer數(shù)據(jù)庫肤京,由于業(yè)務(wù)需要颊艳,不能先進(jìn)行數(shù)據(jù)遷移再上線,必須上線同時(shí)實(shí)現(xiàn)平滑水平遷庫忘分,做到用戶無感知棋枕。
- 如何保證遷庫前后的數(shù)據(jù)一致性?
- 如何處理多數(shù)據(jù)源的數(shù)據(jù)一致性問題妒峦?
- redis新集群與訂單系統(tǒng)的redis集群也要做到平滑遷庫重斑。
- 原本依賴順風(fēng)車相關(guān)的業(yè)務(wù)操作抽象成遠(yuǎn)程服務(wù)
SqlServert ---> Mysql 數(shù)據(jù)遷移方案 -- 雙寫
經(jīng)過團(tuán)隊(duì)的調(diào)研、討論最終定下數(shù)據(jù)遷移方案:
大致思路是新系統(tǒng)上線時(shí)以及上線后的一段時(shí)間繼續(xù)采取之前的做法:從sqlServer數(shù)據(jù)庫存取數(shù)據(jù)肯骇,上線后 立即打開開始數(shù)據(jù)同步并采取
操作窥浪,數(shù)據(jù)同步過程中在對sqlServer進(jìn)行讀寫操作時(shí)同時(shí)對mySql數(shù)據(jù)庫進(jìn)行同樣的一次讀寫操作祖很。 在數(shù)據(jù)同步完成之后確保兩個(gè)數(shù)據(jù)庫的數(shù)據(jù)完全一致后,關(guān)閉雙寫開關(guān)漾脂,此后順風(fēng)車的一切讀寫操作全都由mysql數(shù)據(jù)庫完成假颇,至此完成
。流程圖如下圖所示:
image.png
雙寫開關(guān):代碼中采取硬編碼的形式骨稿, 在zk配置中心配置雙寫開關(guān)笨鸡,以便靈活的對開/閉 開關(guān)不用修改代碼。配置信息和關(guān)鍵代碼如下:
AEHA%FX}H8AKCM(NX1(28R6.png
摒棄之前用一次去配置中心取一次的操作坦冠,改為配置類(單例)初始化時(shí)讀取到本項(xiàng)目的所有配置中心數(shù)據(jù)形耗, 通過與字段對應(yīng)關(guān)系
給字段賦值。大大節(jié)省了 從配置中心取數(shù)據(jù)的高頻操作所耗費(fèi)的時(shí)間辙浑。
當(dāng)然激涤,如果配置中心的數(shù)據(jù)被修改,采用配置中心的機(jī)制第一時(shí)間給字段重新賦值例衍,注意為了保證實(shí)例變量在線程之間的可見性昔期,字段用
修飾。
@Data
public final class ConfCenterProperties {
/** 遷移開關(guān) true: myql false: sqlServer*/
@NotNull
private volatile Boolean transferSwitch;
/** 配置中心數(shù)據(jù)源名稱 與 當(dāng)前類字段名 對應(yīng)關(guān)系映射 */
private static final Map<String, String> FIELD_MAP = new HashMap<>();
// 新增字段需要配置該對應(yīng)關(guān)系
static {
FIELD_MAP.put("xxx.xxx.switch", "transferSwitch");
}
private static final ConfCenterProperties instance = new ConfCenterProperties();
private static final Logger LOGGER = LoggerFactory.getLogger(ConfCenterProperties.class);
//實(shí)例化時(shí)反射賦值
private ConfCenterProperties() {
//獲取所有的配置數(shù)據(jù)佛玄,遍歷匹配實(shí)例變量并反射賦值
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 + "不可為空累澡,請檢查配置中心配置梦抢,數(shù)據(jù)源為" + key);
}
} catch (Exception e) {
throw new IllegalStateException("初始化配置中心數(shù)據(jù)異常:" + e.getMessage());
}
}
}
//回調(diào)方法
public static void onEvent(DataSourceTransport dataSourceTransport) {
//配置中心數(shù)據(jù)發(fā)生變化 修改實(shí)例變量屬性值 代碼省略……
}
//配置中心監(jiān)視器 回調(diào)
public class ConfCenterListener implements DataChangeListener {
@Override
public void call(DataSourceTransport dataSourceTransport) {
ConfCenterProperties.onEvent(dataSourceTransport);
}
}
數(shù)據(jù)同步: 聯(lián)系架構(gòu)、DBA進(jìn)行數(shù)據(jù)庫數(shù)據(jù)遷移操作愧哟,他們有成熟的一套體系奥吩,業(yè)務(wù)人員只需要關(guān)注自身業(yè)務(wù)即可。 通過與DBA同事交流得知 數(shù)據(jù)遷移是采用xxxx的方式進(jìn)行蕊梧,想進(jìn)一步深入了解的小伙伴可以霞赫。點(diǎn)擊此鏈接
如何保證遷庫后 的數(shù)據(jù)一致性:
重點(diǎn)來了~~~由于業(yè)務(wù)繁忙,如何確保兩個(gè)數(shù)據(jù)庫數(shù)據(jù)的一致性是本次需求的重中之重肥矢。稍有不慎產(chǎn)生臟數(shù)據(jù)或數(shù)據(jù)丟失就會(huì)造成嚴(yán)重的線上事故端衰。一大波用戶投訴又會(huì)接踵而至,事件單又會(huì)處理的讓人頭皮搔更短 甘改,渾欲不勝簪旅东。話不多說先來雙寫遷移圖壓壓鯨@#¥%……&*
image.png
熱心讀者趙二狗提出如下疑問@!#$%^&*(
數(shù)據(jù)遷移完成之后,就能夠切到新庫提供服務(wù)了么十艾?
答案是肯定的抵代,因?yàn)榍爸貌襟E進(jìn)行了雙寫,所以理論上數(shù)據(jù)遷移完之后忘嫉,新庫與舊庫的數(shù)據(jù)應(yīng)該完全一致荤牍。
怎么證明數(shù)據(jù)遷移完成之后數(shù)據(jù)就完全一致了呢案腺?
這就得分為以下三種情況分別討論
1.
雙insert操作:舊庫新庫都插入了數(shù)據(jù),數(shù)據(jù)一致性沒有被破壞
2.
雙delete操作:delete的數(shù)據(jù)屬于[min,now]
范圍內(nèi)和范圍外分別討論
3.
雙update操作:可以認(rèn)為update操作是一個(gè)delete加一個(gè)insert操作的復(fù)合操作康吵,所以數(shù)據(jù)仍然是一致的
上線時(shí)開啟雙寫開關(guān)救湖,那什么時(shí)候關(guān)閉雙寫開關(guān)呢?
當(dāng)我們通過DBA提供的工具發(fā)現(xiàn)兩個(gè)數(shù)據(jù)庫的數(shù)據(jù)并無并無差異涎才,完全一致的時(shí)候鞋既,就可以關(guān)閉雙寫開關(guān)。之后的讀寫操作全部交由mysql處理耍铜。當(dāng)然啦邑闺,如果過程中發(fā)現(xiàn)問題,我們可以清空新庫的數(shù)據(jù)重新開始同步數(shù)據(jù)棕兼,直至兩個(gè)數(shù)據(jù)庫的數(shù)據(jù)完全一致為止
舊庫進(jìn)行了insert操作插入一條數(shù)據(jù)陡舅,新庫同樣也要進(jìn)行一次insert操作,舊庫Insert操作是主鍵遞增伴挚,那么這個(gè)新庫Insert操作的id從何而來呢靶衍?
這個(gè)不難解決,舊庫執(zhí)行完insert操作之后我們可以拿到insert后的主鍵id給新庫insert操作使用就好了
如果一個(gè)service方法中茎芋,既有sqlServer的寫操作又有mysql的寫操作颅眶,假設(shè)其中一個(gè)操作失敗怎么保證另外的操作會(huì)跟著回滾呢? 事務(wù)的一致性如何保證田弥?
小老弟涛酗,你可算問到點(diǎn)子上了,這個(gè)問題一句兩句說不清楚偷厦,先接著往后看吧商叹。你會(huì)得到你想要的答案~~~~
多數(shù)據(jù)源事務(wù)處理
由于項(xiàng)目中需要對sqlServer數(shù)據(jù)庫和mySql數(shù)據(jù)庫進(jìn)行雙寫操作,所以不可避免的要配置兩個(gè)數(shù)據(jù)源只泼。那么一系列操蛋的問題接踵而來剖笙。
事務(wù)四大特性
:一個(gè)事務(wù)中的操作要么全部成功要么全部失敗
:在一個(gè)事務(wù)執(zhí)行之前和執(zhí)行之后數(shù)據(jù)庫都必須處于一致性狀態(tài)。
:并發(fā)的事務(wù)是相互隔離的请唱。
:意味著當(dāng)系統(tǒng)或介質(zhì)發(fā)生故障時(shí)弥咪,確保已提交事務(wù)的更新不能丟失。即一旦一個(gè)事務(wù)提交籍滴,保證它對數(shù)據(jù)庫中數(shù)據(jù)的改變應(yīng)該是永久性的酪夷,耐得住任何系統(tǒng)故障。持久性通過數(shù)據(jù)庫備份和恢復(fù)來保證孽惰。
spring事務(wù)抽象
在spring當(dāng)中為我們的數(shù)據(jù)訪問層提供了很多抽象晚岭,在這些抽象的幫助下面我們可以非常方便的在不同的框架當(dāng)中使用一樣的方式來進(jìn)行數(shù)據(jù)操作, 其中最重要的一個(gè)抽象就是勋功。
spring的事務(wù)抽象: 一致的事務(wù)模型
spring提供了一致的事務(wù)模型坦报,不管是用JDBC還是Mybatis或者是Hibernate來操作數(shù)據(jù)庫库说,也不管我們是使用的datasource的事務(wù)還是使用JTA的事務(wù),在這個(gè)事務(wù)抽象里面都能給他們很好的統(tǒng)一在一起片择。
- JDBC/Hibernate/myBatis
- DataSource/JTA
事務(wù)抽象的核心接口
//事務(wù)抽象核心接口
public interface PlatformTransactionManager {
//獲取事務(wù)信息
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事務(wù)
void commit(TransactionStatus var1) throws TransactionException;
//回滾事務(wù)
void rollback(TransactionStatus var1) throws TransactionException;
PlatformTransactionManager
DataSourceTransactionManager
JtaTransactionManager
HibernateTransactionManger
TransactionDefinition
通過TransactionDefinition 可以獲取TransactionStatus 潜的,包含了事務(wù)的只讀狀態(tài)、回滾狀態(tài)等等信息字管。
Propagation
傳播特性Isolation
隔離性Timeout
超時(shí)設(shè)置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();
事務(wù)傳播特性
默認(rèn) required
new和 nested的區(qū)別:
new 始終啟動(dòng)一個(gè)新事務(wù)啰挪,跟外層的事務(wù)沒有關(guān)聯(lián)
nested 兩個(gè)事務(wù)有關(guān)聯(lián),外部事務(wù)回滾嘲叔,內(nèi)嵌事務(wù)也會(huì)回滾 保持一致
事務(wù)隔離特性
默認(rèn)-1 完全取決于數(shù)據(jù)庫 可以根據(jù)實(shí)際需求去做設(shè)定或者默認(rèn) 使用數(shù)據(jù)庫的默認(rèn)的隔離特性
編程式事務(wù)
TransactionTemplate
最基本的最簡單的方式就是使用TransactionTemplate亡呵,當(dāng)然你也可以說我就是這么傲嬌,我就想直接使用JDBC原生的方法硫戈,用Connection在里面去做開始事務(wù)锰什,提交事務(wù),回滾事務(wù)丁逝,當(dāng)然也可以汁胆。
- TrancastionCallBack 有返回值
- TranscationCallBackWithoutResult 沒有返回值
PlatformTransactionManger- 可以傳入TransactionDefinition進(jìn)行定義
@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());
//讓事務(wù)回滾
transactionStatus.setRollbackOnly();
}
});
log.info("after count:{}",getCount());
}
事務(wù)開啟前0條數(shù)據(jù),事務(wù)過程當(dāng)中 1條數(shù)據(jù)霜幼,事務(wù)回滾后0條數(shù)據(jù)
聲明式事務(wù)
利用spring的動(dòng)態(tài)代理在我們的目標(biāo)方法上加了一層封裝(環(huán)繞通知)嫩码,幫助我們進(jìn)行模版式的事務(wù)操作。
<tx:annotation-driven/> 開始事務(wù)功
<!-- 配置事務(wù)注解驅(qū)動(dòng) -->
<tx:annotation-driven transaction-manager="transactionManager"/>
![]()
@EnableTranscationManagement
開始事務(wù)功能 大勢所趨本文介紹注解方法
@Transaction
- transactionMager 一般是datasoureTransactionManger辛掠,如果就一個(gè)谢谦,就會(huì)默認(rèn)選中這個(gè)
- 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("回滾術(shù)");
}
大多數(shù)人存在的誤區(qū)
有了聲明式事務(wù)之后,許多人都會(huì)認(rèn)為只要在調(diào)用dao的service方法上加上
@Transactional
就萬事大吉了没咙。而事實(shí)情況并非如此猩谊。先來看一段代碼~~
hellow