自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

工作日志,多租戶模式下的數(shù)據(jù)備份和遷移

存儲(chǔ) 存儲(chǔ)軟件
記錄和分享一篇工作中遇到的奇難雜癥。目前做的項(xiàng)目是多租戶模式。一套系統(tǒng)管理多個(gè)項(xiàng)目,用戶登錄不同的項(xiàng)目加載不同的數(shù)據(jù)。

 工作日志,多租戶模式下的數(shù)據(jù)備份和遷移

記錄和分享一篇工作中遇到的奇難雜癥。目前做的項(xiàng)目是多租戶模式。一套系統(tǒng)管理多個(gè)項(xiàng)目,用戶登錄不同的項(xiàng)目加載不同的數(shù)據(jù)。除了一些系統(tǒng)初始化的配置表外,各項(xiàng)目之間數(shù)據(jù)相互獨(dú)立。前期選擇了共享數(shù)據(jù)表的隔離方案,為后期的數(shù)據(jù)遷移挖了一個(gè)大坑。這里記錄填坑的思路。可能不優(yōu)雅,僅供參考。

[[272545]]

多租戶

多租戶是一種軟件架構(gòu),在同一臺(tái)(組)服務(wù)器上運(yùn)行單個(gè)實(shí)例,能為多個(gè)租戶提供服務(wù)。以實(shí)際例子說(shuō)明,一套能源監(jiān)控系統(tǒng),可以為A產(chǎn)業(yè)園提供服務(wù),也可以為B產(chǎn)業(yè)園提供服務(wù)。A的管理員登錄能源監(jiān)控系統(tǒng)只會(huì)看到A產(chǎn)業(yè)園相關(guān)的數(shù)據(jù)。同樣的道理,B產(chǎn)業(yè)園也是一樣。多住戶模式最重要的就是數(shù)據(jù)之間的獨(dú)立。其最大的局限性在于對(duì)租戶定制化開(kāi)發(fā)困難很大。適合通用的業(yè)務(wù)場(chǎng)景。

數(shù)據(jù)隔離方案

獨(dú)立數(shù)據(jù)庫(kù)

顧名思義,一個(gè)租戶獨(dú)享一個(gè)數(shù)據(jù)庫(kù),其隔離級(jí)別最強(qiáng),數(shù)據(jù)安全性最高,數(shù)據(jù)的備份和恢復(fù)最方便。對(duì)數(shù)據(jù)獨(dú)立性要求很高,數(shù)據(jù)的擴(kuò)張性要求較多的租戶可以考慮使用。或者錢給的多也可以考慮。畢竟該模式下的硬件成本較高。代碼成本較低,Hibernate已經(jīng)提供DATABASE的實(shí)現(xiàn)。

共享數(shù)據(jù)庫(kù)、獨(dú)立 Schema

多個(gè)租戶共有一個(gè)數(shù)據(jù)庫(kù),每個(gè)租戶擁有屬于自己的Schema(Schema表示數(shù)據(jù)庫(kù)對(duì)象集合,它包含:表,視圖,存儲(chǔ)過(guò)程,索引等等對(duì)象)。其隔離級(jí)別較強(qiáng),數(shù)據(jù)安全性較高,數(shù)據(jù)的備份和恢復(fù)較為麻煩。數(shù)據(jù)庫(kù)出了問(wèn)題會(huì)影響到所有租戶。Hibernate也提供SCHEMA的實(shí)現(xiàn)。

共享數(shù)據(jù)庫(kù)、共享 Schema、共享數(shù)據(jù)表

多個(gè)租戶共享一個(gè)數(shù)據(jù)庫(kù),一個(gè)Schema,一張數(shù)據(jù)表。各租戶之間通過(guò)字段區(qū)分。其隔離級(jí)別最低,數(shù)據(jù)安全性最低,數(shù)據(jù)的備份和恢復(fù)最麻煩(讓我哭一分鐘😭)。若一張表出現(xiàn)問(wèn)題會(huì)影響到所有租戶。其代碼工作量也是最多,因?yàn)镠ibernate(5.0.3版本)并沒(méi)有支持DISCRIMINATOR模式,目前還只是計(jì)劃支持。其模式最大的好處就是用最少的服務(wù)器支持最多的租戶。

業(yè)務(wù)場(chǎng)景

在我們的能源管理的系統(tǒng)中,多個(gè)租戶就是多個(gè)項(xiàng)目。將需要數(shù)據(jù)獨(dú)立的數(shù)據(jù)表通過(guò)ProjectID區(qū)分。而一些系統(tǒng)初始化的配置表則可以數(shù)據(jù)共享。怎么用盡可能少的代碼來(lái)管理每個(gè)租戶呢?這里提出我個(gè)人的思路。

多租戶的實(shí)現(xiàn)

第一步:用戶登錄時(shí)獲取當(dāng)前項(xiàng)目,并保存到上下文中。

第二步:通過(guò)EntityListeners注解監(jiān)聽(tīng),在實(shí)體被創(chuàng)建時(shí)將當(dāng)前項(xiàng)目ID保存到數(shù)據(jù)庫(kù)中。

第三步:通過(guò)自定義攔截器,攔截需要數(shù)據(jù)隔離的sql語(yǔ)句,重新拼接查詢條件。

將當(dāng)前項(xiàng)目保存到上下文中,不同的安全框架實(shí)現(xiàn)的方法也有所不同,實(shí)現(xiàn)的方式也多種多樣,這里就不貼出代碼。

通過(guò)EntityListeners注解可以對(duì)實(shí)體屬性變化的跟蹤,它提供了保存前,保存后,更新前,更新后,刪除前,刪除后等狀態(tài),就像是攔截器一樣。這里我們可以用到PrePersist 在保存前將項(xiàng)目ID賦值

  1. @MappedSuperclass 
  2. @EntityListeners(ProjectIdListener::class) 
  3. @Poko 
  4. class TenantModel: AuditModel() { 
  5.     var projectId: String? = null 
  6. class ProjectIdListener { 
  7.  
  8.     @PrePersist 
  9.     fun setProjectId(resultObj: Any) { 
  10.         try { 
  11.             val projectIdProperty = resultObj::class.java.superclass.getDeclaredField("projectId"
  12.             if (projectIdProperty.type == String::class.java) { 
  13.                 projectIdProperty.isAccessible = true 
  14.                 projectIdProperty.set(resultObj, ContextUtils.getCurrentProjectId()) 
  15.             } else { 
  16.             } 
  17.         } catch (ex: Exception) { 
  18.         } 
  19.     } 

自定義SQL攔截器,通過(guò)實(shí)現(xiàn)StatementInspector接口,實(shí)現(xiàn)inspect方法即可。不同的業(yè)務(wù)邏輯,實(shí)現(xiàn)的邏輯也不一樣,這里就不貼代碼了。

注意:

一)、以上是kotlin代碼,IDEA支持Kotlin和Java代碼的互轉(zhuǎn)。

二)、需要數(shù)據(jù)隔離的實(shí)體,繼承TenantModel類即可,沒(méi)有繼承的實(shí)體默認(rèn)為數(shù)據(jù)共享。

三)、ContextUtils是自定義獲取上下文的工具類。

數(shù)據(jù)備份

業(yè)務(wù)分析

到了文章的重點(diǎn)。數(shù)據(jù)的備份目的是數(shù)據(jù)遷移和數(shù)據(jù)的還原。友好的備份格式可以為數(shù)據(jù)遷移減少很多工作量。剛開(kāi)始覺(jué)得這個(gè)需求很簡(jiǎn)單,MySQL的數(shù)據(jù)備份做過(guò)很多次,也很簡(jiǎn)單。但數(shù)據(jù)備份不僅僅是數(shù)據(jù)恢復(fù),還有數(shù)據(jù)遷移的功能(A項(xiàng)目下的數(shù)據(jù)備份后,可以導(dǎo)入的B項(xiàng)目下)。這下就有意思了。我們理一理:

一)、數(shù)據(jù)備份是數(shù)據(jù)隔離的。A項(xiàng)目數(shù)據(jù)備份,只能備份A項(xiàng)目下的數(shù)據(jù)。

二)、備份的數(shù)據(jù)用于數(shù)據(jù)恢復(fù)。

三)、備份的數(shù)據(jù)用于數(shù)據(jù)遷移,之前存在的關(guān)聯(lián)數(shù)據(jù)要重新綁定關(guān)聯(lián)關(guān)系。

四)、數(shù)據(jù)恢復(fù)和遷移過(guò)程中,注意重復(fù)導(dǎo)入和事務(wù)問(wèn)題。

針對(duì)上面的分析,一般都有會(huì)三種解決思路:

一)、用MySQL自帶的命令導(dǎo)入和導(dǎo)出。

二)、找已經(jīng)做好的輪子。(如果有,請(qǐng)麻煩告知一下)

三)、自己實(shí)現(xiàn)將數(shù)據(jù)轉(zhuǎn)為JSON數(shù)據(jù),再由JSON數(shù)據(jù)導(dǎo)入的功能。

因?yàn)樾枨笕托枨笏牡奶厥庑裕琈ySQL自帶的命令很難滿足,也沒(méi)有合適的輪子。只能自己實(shí)現(xiàn),這樣做也更放心點(diǎn)。

實(shí)現(xiàn)流程

第一步:確定表的順序。項(xiàng)目之間數(shù)據(jù)遷移后,需要重新綁定表的關(guān)聯(lián)關(guān)系,優(yōu)先導(dǎo)入導(dǎo)出沒(méi)有外鍵關(guān)聯(lián)的表。

第二步:遍歷每張表,將數(shù)據(jù)轉(zhuǎn)成JSON格式數(shù)據(jù)一行行寫(xiě)入到文本文件中。

導(dǎo)出數(shù)據(jù)偽代碼:

  1. fun exportSystemData(request: HttpServletRequest, response: HttpServletResponse) { 
  2.     // 校驗(yàn)權(quán)限 
  3.     checkAuthority("導(dǎo)出系統(tǒng)數(shù)據(jù)"
  4.     // 獲取當(dāng)前項(xiàng)目 
  5.     val currentProjectId = ContextUtils.getCurrentProjectId() 
  6.     val systemFilePath = "${attachmentPath}system${File.separator}$currentProjectId" 
  7.     val file = File(systemFilePath) 
  8.     if (!file.exists()) { 
  9.         file.mkdirs() 
  10.     } 
  11.     // 獲取數(shù)據(jù)獨(dú)立的表名(方便查詢)和類名的全路徑(方便反射) 
  12.     val moreProjectEntityMap = CommonUtils.getMoreProjectEntity() 
  13.     moreProjectEntityMap.remove(CommonUtils.toUnderline(SystemLog::class.simpleName)) 
  14.     moreProjectEntityMap.remove(CommonUtils.toUnderline(AlarmRecord::class.simpleName)) 
  15.     // 生成文件 
  16.     moreProjectEntityMap.forEach { entry -> 
  17.         var tableFile: FileWriter? = null 
  18.         try { 
  19.             tableFile = FileWriter(File(systemFilePath, "${entry.key}.txt")) 
  20.             dataManagementService.findAll(Class.forName(entry.value)).forEach { 
  21.                 tableFile.write("${JSONObject.toJSONString(it)} \n"
  22.             } 
  23.         } catch (e: Exception) { 
  24.             e.printStackTrace() 
  25.         } finally { 
  26.             tableFile?.let { 
  27.                 it.flush() 
  28.                 it.close() 
  29.             } 
  30.         } 
  31.     } 
  32.     // 壓縮成一個(gè)文件 
  33.     fileUtil.zip(systemFilePath) 
  34.     file.listFiles().forEach { it.delete() } 
  35.     fileUtil.downloadAttachment("$systemFilePath.zip", response) 

數(shù)據(jù)遷移

業(yè)務(wù)分析

備份后的數(shù)據(jù)有兩個(gè)用途。第一是數(shù)據(jù)還原;最重要的是數(shù)據(jù)遷移。將A項(xiàng)目中的配置導(dǎo)入到B項(xiàng)目中,可以提高用戶的效率。數(shù)據(jù)還原最簡(jiǎn)單,這里重點(diǎn)介紹數(shù)據(jù)遷移的思路(可能不太合理)

數(shù)據(jù)遷移最麻煩的就是新創(chuàng)建后的數(shù)據(jù)如何重新綁定主外表的關(guān)系。其次就是如果導(dǎo)入過(guò)程中失敗,事務(wù)的處理問(wèn)題。為了處理這兩個(gè)問(wèn)題,我選擇新增一張表維護(hù)新舊ID的遷移記錄。每次導(dǎo)入成功后就在表中保存數(shù)據(jù)。這樣可以避免重復(fù)導(dǎo)入的情況。也為新數(shù)據(jù)重新綁定主外關(guān)系做準(zhǔn)備。

實(shí)現(xiàn)步驟

第一步:解壓上傳后的文件,并按照指定的排序順序讀取解壓后的文件。

第二步:一行行讀取數(shù)據(jù),通過(guò)反射將JSON格式字符串轉(zhuǎn)為對(duì)象。遍歷對(duì)象的值將舊ID根據(jù)數(shù)據(jù)遷移記錄替換成遷移后的新ID。

第三步:檢擦數(shù)據(jù)遷移記錄表中是否已經(jīng)存在遷移記錄,若沒(méi)有則插入數(shù)據(jù)并記錄日志。

第四步:若數(shù)據(jù)遷移記錄表中已經(jīng)存在記錄,則更新數(shù)據(jù)。

第五步:讀取第二行數(shù)據(jù),重復(fù)執(zhí)行。

數(shù)據(jù)恢復(fù)偽代碼

  1. fun importSystemData(file: MultipartFile, request: HttpServletRequest) { 
  2.     checkAuthority("導(dǎo)入系統(tǒng)數(shù)據(jù)"
  3.     val currentProjectId = ContextUtils.getCurrentProjectId() 
  4.     val systemFilePath = "${attachmentPath}system" 
  5.     val tempFile = File(systemFilePath, file.originalFilename) 
  6.     val fileOutputStream = FileOutputStream(tempFile) 
  7.     fileOutputStream.write(file.bytes) 
  8.     fileOutputStream.close() 
  9.     // 獲取排序后遷移表 
  10.     val moreProjectEntityMap = CommonUtils.getMoreProjectEntity() 
  11.     moreProjectEntityMap.remove(CommonUtils.toUnderline(SystemLog::class.simpleName)) 
  12.     val files: MutableMap<String, File> = mutableMapOf() 
  13.     fileUtil.unzip(tempFile.absoluteFile, systemFilePath, "").forEach { 
  14.         files[it!!.nameWithoutExtension] = it 
  15.     } 
  16.     val dataTransferHistories = dataTransferHistoryRepository.findByProjectId(currentProjectId).toMutableList() 
  17.     try { 
  18.         moreProjectEntityMap.keys.forEach {  fileName -> 
  19.             val tableFile = files.getOrDefault(fileName, null) ?: return@forEach 
  20.             val entity = Class.forName(moreProjectEntityMap[fileName]) 
  21.             tableFile.forEachLine { dataStr -> 
  22.                 val data = JSONObject.parseObject(dataStr, entity) 
  23. //              獲取對(duì)象所有屬性 
  24.                 val fieldMap = CommonUtils.getEntityAllField(data) 
  25. //              獲取數(shù)據(jù)遷移的舊ID 
  26.                 val id = fieldMap["id"]!!.get(data) as String 
  27.                 val dataTransferHistory = dataTransferHistories.find { it.oldId == id } 
  28. //              重新綁定遷移數(shù)據(jù)后的id 
  29.                 handleEntityData(data, fieldMap, moreProjectEntityMap.values.toList(), dataTransferHistories) 
  30.                 fieldMap["projectId"]!!.set(data, currentProjectId) 
  31.                 if (null == dataTransferHistory || null == dataManagementService.getByIdElseNull(dataTransferHistory.newId, entity)) { 
  32.                     val saved = dataManagementService.create(data, entity) 
  33. //                  綁定舊ID和新ID的關(guān)系 
  34.                     val savedId = CommonUtils.getEntityAllField(saved)["id"]!!.get(saved) as String 
  35.                     if (null == dataTransferHistory) { 
  36.                         dataTransferHistories.add(DataTransferHistory(id, savedId, currentProjectId, fileName)) 
  37.                     } 
  38.                 } else { 
  39.                     fieldMap["id"]!!.set(data, dataTransferHistory.newId) 
  40.                     dataManagementService.update(data, entity) 
  41.                 } 
  42.             } 
  43.         } 
  44.     } catch (e: Exception) { 
  45.         e.printStackTrace() 
  46.         throw IllegalArgumentException("數(shù)據(jù)導(dǎo)入失敗"
  47.     } finally { 
  48.         tempFile.delete() 
  49.         files.values.forEach { it.delete() } 
  50.         recordDataTransferHistory(dataTransferHistories) 
  51.     } 
  52.  
  53. // 記錄數(shù)據(jù)遷移 
  54. private fun recordDataTransferHistory(dataTransferHistories: MutableList<DataTransferHistory>) { 
  55.     dataTransferHistoryRepository.saveAll(dataTransferHistories) 
  56.  
  57. // 重新綁定主外關(guān)系表 
  58. fun handleEntityData(sourceClass: Any, fieldMap: MutableMap<String, Field>, classPaths: List<String>, dataTransferHistories: MutableList<DataTransferHistory>) { 
  59.     val currentProjectId = ContextUtils.getCurrentProjectId() 
  60.     fieldMap.values.forEach { field -> 
  61.         val classPath = field.type.toString().split(" ").last() 
  62.         // 一對(duì)多或多對(duì)多關(guān)系 
  63.         if (classPath == "java.util.List") { 
  64.             val listValue = field.get(sourceClass) as List<*> 
  65.             listValue.forEach { listObj -> 
  66.                 listObj?.let { changeOldRelId4NewData(it, dataTransferHistories, currentProjectId) } 
  67.             } 
  68.         } 
  69.         // 一對(duì)一或多對(duì)一關(guān)系 
  70.         if (classPaths.contains(classPath)) { 
  71.             val value = field.get(sourceClass)?: return@forEach 
  72.             changeOldRelId4NewData(value, dataTransferHistories, currentProjectId) 
  73.         } 
  74.         // 字符串ID關(guān)聯(lián) 
  75.         if (classPath == "java.lang.String" && null != field.get(sourceClass)) { 
  76.             var oldId = field.get(sourceClass).toString() 
  77.             dataTransferHistories.forEach { 
  78.                 oldId = oldId.replace(it.oldId, it.newId) 
  79.             } 
  80.             field.set(sourceClass, oldId) 
  81.         } 
  82.     } 
  83.  
  84. fun changeOldRelId4NewData(data: Any, dataTransferHistories: MutableList<DataTransferHistory>, currentProjectId: String) { 
  85.     val fieldMap = CommonUtils.getEntityAllField(data) 
  86.     fieldMap.values.forEach { field -> 
  87.         if (field.type.toString().contains("java.lang.String") && null != field.get(data)) { 
  88.             var oldId = field.get(data).toString() 
  89.             dataTransferHistories.forEach { 
  90.                 oldId = oldId.replace(it.oldId, it.newId) 
  91.             } 
  92.             field.set(data, oldId) 
  93.         } 
  94.     } 
  95.     fieldMap["projectId"]!!.set(data, currentProjectId) 
  96. /** 
  97.  * 數(shù)據(jù)遷移記錄表 
  98.  */ 
  99. @Entity 
  100. @Table(uniqueConstraints = [UniqueConstraint(columnNames = ["oldId""projectId"])]) 
  101. data class DataTransferHistory ( 
  102.  
  103.         var oldId: String = ""
  104.         var newId: String = ""
  105.         var projectId: String = ""
  106.         var tableName: String = ""
  107.         var createTime: Instant = Instant.now(), 
  108.         @Id 
  109.         @GenericGenerator(name = "idGenerator", strategy = "uuid"
  110.         @GeneratedValue(generator = "idGenerator"
  111.         var id: String = "" 
  112.  

到這里就結(jié)束了,以上思路僅供參考。

小結(jié)

一)、數(shù)據(jù)備份需要項(xiàng)目獨(dú)立

二)、通過(guò)項(xiàng)目ID 區(qū)分備份的數(shù)據(jù)是用來(lái)數(shù)據(jù)還原還是數(shù)據(jù)遷移

三)、數(shù)據(jù)遷移過(guò)程中需要考慮數(shù)據(jù)重復(fù)導(dǎo)入的問(wèn)題

四)、數(shù)據(jù)遷移過(guò)程中需要重新綁定主外鍵的關(guān)聯(lián)

五)、第三和第四點(diǎn)可以通過(guò)記錄數(shù)據(jù)遷移表做輔助

六)、數(shù)據(jù)遷移過(guò)程盡量避免刪除操作。避免對(duì)其他項(xiàng)目造成影響。

責(zé)任編輯:武曉燕 來(lái)源: Segmentfault
相關(guān)推薦

2015-08-12 15:46:02

SaaS多租戶數(shù)據(jù)存儲(chǔ)

2015-04-02 11:04:27

云應(yīng)用SaaSOFBIZ

2011-03-31 12:17:07

Cacti備份

2017-10-23 21:19:10

數(shù)據(jù)中心SDN軟件定義網(wǎng)絡(luò)

2023-11-06 08:26:11

Spring微服務(wù)架構(gòu)

2024-04-02 09:01:45

2013-11-26 17:29:43

思科呼叫中心多租戶

2011-10-21 12:29:38

IPv6雙協(xié)議DNS

2023-12-14 12:26:16

SaaS數(shù)據(jù)庫(kù)方案

2019-10-25 14:17:00

邊緣計(jì)算數(shù)據(jù)中心多租戶數(shù)據(jù)中心

2020-09-15 07:00:00

SaaS架構(gòu)架構(gòu)

2020-10-16 08:57:51

云平臺(tái)之多租戶的實(shí)踐

2013-04-15 09:52:13

程序員

2022-05-13 07:26:28

策略模式設(shè)計(jì)模式

2022-02-23 08:55:06

數(shù)據(jù)遷移分庫(kù)分表數(shù)據(jù)庫(kù)

2020-07-30 09:44:26

數(shù)據(jù)中心IT技術(shù)

2023-06-07 13:50:00

SaaS多租戶系統(tǒng)

2022-01-12 17:39:16

Spring多租戶數(shù)據(jù)

2021-03-17 08:11:21

SQL工作日數(shù)據(jù)

2010-05-21 12:52:55

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)