• “两学一做”在山西——黄河新闻网 2019-07-13
  • 内地生报读香港高校本科人数持续下跌 2019-07-13
  • 【学习时刻】人民大学王义桅:金砖合作的“自信”与“自觉” 2019-07-12
  • 女子请“私家侦探”被骗3万 警方循线捣毁诈骗团伙 2019-07-11
  • 【学习时刻】北交大马院院长韩振峰:高校思想政治工作必须牢牢把握三大根本问题 2019-07-11
  • 全国“非遗”保护工作先进名单公布 2019-07-01
  • 紫光阁中共中央国家机关工作委员会 2019-06-25
  • 杭州控烟令修改草案拟允许室内设吸烟区,控烟专家:跌破眼镜 2019-06-25
  • 挪用近30万报纸征订款赌博 河南一报社聘用制干部获刑 2019-06-23
  • 2016年,有1145家上市公司大小非减持了3600亿元,还有210名上市公司高管减持了1400亿元。IPO已成了造就成千上万个十亿、百亿富豪的捷径, 2019-06-21
  • 专家“把脉”中国电影市场:提升品质方能逆袭 2019-06-21
  • “善款资助副局长儿子留学”真相须尽快落地 2019-06-19
  • 21岁女护士失联2天后确认遇害 嫌疑人为其前男友 2019-06-19
  • 中国地质公园名录旅行地中国国家地理网 2019-06-13
  • 玄关运用有四大原则 用的好才能财旺挡煞聚财 ——凤凰网房产 2019-06-10
  • 工作日志,多租户模式下的数据备份和迁移

    广东十一选5一定牛 www.aavbg.com 工作日志,多租户模式下的数据备份和迁移

    记录和分享一篇工作中遇到的奇难杂症。目前做的项目是多租户模式。一套系统管理多个项目,用户登录不同的项目加载不同的数据。除了一些系统初始化的配置表外,各项目之间数据相互独立。前期选择了共享数据表的隔离方案,为后期的数据迁移挖了一个大坑。这里记录填坑的思路??赡懿挥叛?,仅供参考。

    文章目录

    多租户

    多租户是一种软件架构,在同一台(组)服务器上运行单个实例,能为多个租户提供服务。以实际例子说明,一套能源监控系统,可以为A产业园提供服务,也可以为B产业园提供服务。A的管理员登录能源监控系统只会看到A产业园相关的数据。同样的道理,B产业园也是一样。多住户模式最重要的就是数据之间的独立。其最大的局限性在于对租户定制化开发困难很大。比较适合通用的业务场景。

    数据隔离方案

    独立数据库

    顾名思义,一个租户独享一个数据库,其隔离级别最强,数据安全性最高,数据的备份和恢复最方便。对数据独立性要求很高,数据的扩张性要求较多的租户可以考虑使用?;蛘咔亩嘁部梢钥悸?。毕竟该模式下的硬件成本较高。代码成本较低,Hibernate已经提供DATABASE的实现。

    共享数据库、独立 Schema

    多个租户共有一个数据库,每个租户拥有属于自己的Schema(Schema表示数据库对象集合,它包含:表,视图,存储过程,索引等等对象)。其隔离级别较强,数据安全性较高,数据的备份和恢复较为麻烦。数据库出了问题会影响到所有租户。Hibernate也提供SCHEMA的实现。

    共享数据库、共享 Schema、共享数据表

    多个租户共享一个数据库,一个Schema,一张数据表。各租户之间通过字段区分。其隔离级别最低,数据安全性最低,数据的备份和恢复最麻烦(让我哭一分钟??)。若一张表出现问题会影响到所有租户。其代码工作量也是最多,因为Hibernate(5.0.3版本)并没有支持DISCRIMINATOR模式,目前还只是计划支持。其模式最大的好处就是用最少的服务器支持最多的租户。

    业务场景

    在我们的能源管理的系统中,多个租户就是多个项目。将需要数据独立的数据表通过ProjectID区分。而一些系统初始化的配置表则可以数据共享。怎么用尽可能少的代码来管理每个租户呢?这里提出我个人的思路。

    多租户实现

    第一步:用户登录时获取当前项目,并保存到上下文中。

    第二步:通过EntityListeners注解监听,在实体被创建时将当前项目ID保存到数据库中。

    第三步:通过自定义拦截器,拦截需要数据隔离的sql语句,重新拼接查询条件。

    保存用户登录信息

    将当前项目保存到上下文中,不同的安全框架实现的方法也有所不同,实现的方式也多种多样,这里就不贴出代码。

    数据保存前设置当前项目ID

    通过EntityListeners注解可以对实体属性变化的跟踪,它提供了保存前,保存后,更新前,更新后,删除前,删除后等状态,就像是拦截器一样。这里我们可以用到PrePersist 在保存前将项目ID赋值

    @MappedSuperclass
    @EntityListeners(ProjectIdListener::class)
    @Poko
    class TenantModel: AuditModel() {
        var projectId: String? = null
    }
    class ProjectIdListener {
    
        @PrePersist
        fun setProjectId(resultObj: Any) {
            try {
                val projectIdProperty = resultObj::class.java.superclass.getDeclaredField("projectId")
                if (projectIdProperty.type == String::class.java) {
                    projectIdProperty.isAccessible = true
                    projectIdProperty.set(resultObj, ContextUtils.getCurrentProjectId())
                } else {
                }
            } catch (ex: Exception) {
            }
        }
    }

    拦截项目隔离的sql语句

    自定义SQL拦截器,通过实现StatementInspector接口,实现inspect方法即可。不同的业务逻辑,实现的逻辑也不一样,这里就不贴代码了。

    注意事项

    一)、以上是kotlin代码,IDEA支持Kotlin和Java代码的互转。

    二)、需要数据隔离的实体,继承TenantModel类即可,没有继承的实体默认为数据共享。

    三)、ContextUtils是自定义获取上下文的工具类。

    数据备份

    业务分析

    到了文章的重点。数据的备份目的是数据迁移和数据的还原。友好的备份格式可以为数据迁移减少很多工作量。刚开始觉得这个需求很简单,MySQL的数据备份做过很多次,也很简单。但数据备份不仅仅是数据恢复,还有数据迁移的功能(A项目下的数据备份后,可以导入的B项目下)。这下就有意思了。我们理一理需求:

    一)、数据备份是数据隔离的。A项目数据备份,只能备份A项目下的数据。

    二)、备份的数据可以用于数据恢复。

    三)、备份的数据可以用于数据迁移,之前存在的关联数据要重新绑定。

    四)、数据恢复和迁移过程中,注意重复导入和事务问题。

    针对上面的分析,一般都有会三种解决思路:

    一)、用MySQL自带的命令导入和导出。

    二)、找已经做好的轮子。(如果有,请麻烦告知一下)

    三)、自己实现将数据转为JSON数据,再由JSON数据导入的功能。

    因为需求三和需求四的特殊性,MySQL自带的命令很难满足,也没有合适的轮子。只能自己实现,这样做也更放心点。

    数据备份的步骤

    第一步:确定表的顺序。项目之间数据迁移后,需要重新绑定表的关联关系,优先导入导出没有外键关联的表。

    第二步:遍历每张表,将数据转成JSON格式数据一行行写入到文本文件中。

    导出数据伪代码:

    fun exportSystemData(request: HttpServletRequest, response: HttpServletResponse) {
        // 校验权限
        checkAuthority("导出系统数据")
        // 获取当前项目
        val currentProjectId = ContextUtils.getCurrentProjectId()
        val systemFilePath = "${attachmentPath}system${File.separator}$currentProjectId"
        val file = File(systemFilePath)
        if (!file.exists()) {
            file.mkdirs()
        }
        // 获取数据独立的表名(方便查询)和类名的全路径(方便反射)
        val moreProjectEntityMap = CommonUtils.getMoreProjectEntity()
        moreProjectEntityMap.remove(CommonUtils.toUnderline(SystemLog::class.simpleName))
        moreProjectEntityMap.remove(CommonUtils.toUnderline(AlarmRecord::class.simpleName))
        // 生成文件
        moreProjectEntityMap.forEach { entry ->
            var tableFile: FileWriter? = null
            try {
                tableFile = FileWriter(File(systemFilePath, "${entry.key}.txt"))
                dataManagementService.findAll(Class.forName(entry.value)).forEach {
                    tableFile.write("${JSONObject.toJSONString(it)} \n")
                }
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                tableFile?.let {
                    it.flush()
                    it.close()
                }
            }
        }
        // 压缩成一个文件
        fileUtil.zip(systemFilePath)
        file.listFiles().forEach { it.delete() }
        fileUtil.downloadAttachment("$systemFilePath.zip", response)
    }

    数据迁移

    业务分析

    备份后的数据有两个用途。第一是数据还原;最重要的是数据迁移。将A项目中的配置导入到B项目中,可以提高用户的效率。数据还原最简单,这里重点介绍数据迁移的思路(可能不太合理)

    数据迁移最麻烦的就是新创建后的数据如何重新绑定主外表的关系。其次就是如果导入过程中失败,事务的处理问题。为了处理这两个问题,我选择新增一张表维护新旧ID的迁移记录。每次导入成功后就在表中保存数据。这样可以避免重复导入的情况。也为新数据重新绑定主外关系做准备。

    实现步骤

    第一步:解压上传后的文件,并按照指定的排序顺序读取解压后的文件。

    第二步:一行行读取数据,通过反射将JSON格式字符串转为对象。遍历对象的值将旧ID根据数据迁移记录替换成迁移后的新ID。

    第三步:检擦数据迁移记录表中是否已经存在迁移记录,若没有则插入数据并记录日志。

    第四步:若数据迁移记录表中已经存在记录,则更新数据。

    第五步:读取第二行数据,重复执行。

    数据恢复伪代码

    fun importSystemData(file: MultipartFile, request: HttpServletRequest) {
        checkAuthority("导入系统数据")
        val currentProjectId = ContextUtils.getCurrentProjectId()
        val systemFilePath = "${attachmentPath}system"
        val tempFile = File(systemFilePath, file.originalFilename)
        val fileOutputStream = FileOutputStream(tempFile)
        fileOutputStream.write(file.bytes)
        fileOutputStream.close()
        // 获取排序后迁移表
        val moreProjectEntityMap = CommonUtils.getMoreProjectEntity()
        moreProjectEntityMap.remove(CommonUtils.toUnderline(SystemLog::class.simpleName))
        val files: MutableMap<String, File> = mutableMapOf()
        fileUtil.unzip(tempFile.absoluteFile, systemFilePath, "").forEach {
            files[it!!.nameWithoutExtension] = it
        }
        val dataTransferHistories = dataTransferHistoryRepository.findByProjectId(currentProjectId).toMutableList()
        try {
            moreProjectEntityMap.keys.forEach {  fileName ->
                val tableFile = files.getOrDefault(fileName, null) ?: [email protected]
                val entity = Class.forName(moreProjectEntityMap[fileName])
                tableFile.forEachLine { dataStr ->
                    val data = JSONObject.parseObject(dataStr, entity)
    //              获取对象所有属性
                    val fieldMap = CommonUtils.getEntityAllField(data)
    //              获取数据迁移的旧ID
                    val id = fieldMap["id"]!!.get(data) as String
                    val dataTransferHistory = dataTransferHistories.find { it.oldId == id }
    //              重新绑定迁移数据后的id
                    handleEntityData(data, fieldMap, moreProjectEntityMap.values.toList(), dataTransferHistories)
                    fieldMap["projectId"]!!.set(data, currentProjectId)
                    if (null == dataTransferHistory || null == dataManagementService.getByIdElseNull(dataTransferHistory.newId, entity)) {
                        val saved = dataManagementService.create(data, entity)
    //                  绑定旧ID和新ID的关系
                        val savedId = CommonUtils.getEntityAllField(saved)["id"]!!.get(saved) as String
                        if (null == dataTransferHistory) {
                            dataTransferHistories.add(DataTransferHistory(id, savedId, currentProjectId, fileName))
                        }
                    } else {
                        fieldMap["id"]!!.set(data, dataTransferHistory.newId)
                        dataManagementService.update(data, entity)
                    }
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            throw IllegalArgumentException("数据导入失败")
        } finally {
            tempFile.delete()
            files.values.forEach { it.delete() }
            recordDataTransferHistory(dataTransferHistories)
        }
    }
    
    // 记录数据迁移
    private fun recordDataTransferHistory(dataTransferHistories: MutableList<DataTransferHistory>) {
        dataTransferHistoryRepository.saveAll(dataTransferHistories)
    }
    
    // 重新绑定主外关系表
    fun handleEntityData(sourceClass: Any, fieldMap: MutableMap<String, Field>, classPaths: List<String>, dataTransferHistories: MutableList<DataTransferHistory>) {
        val currentProjectId = ContextUtils.getCurrentProjectId()
        fieldMap.values.forEach { field ->
            val classPath = field.type.toString().split(" ").last()
            // 一对多或多对多关系
            if (classPath == "java.util.List") {
                val listValue = field.get(sourceClass) as List<*>
                listValue.forEach { listObj ->
                    listObj?.let { changeOldRelId4NewData(it, dataTransferHistories, currentProjectId) }
                }
            }
            // 一对一或多对一关系
            if (classPaths.contains(classPath)) {
                val value = field.get(sourceClass)?: [email protected]
                changeOldRelId4NewData(value, dataTransferHistories, currentProjectId)
            }
            // 字符串ID关联
            if (classPath == "java.lang.String" && null != field.get(sourceClass)) {
                var oldId = field.get(sourceClass).toString()
                dataTransferHistories.forEach {
                    oldId = oldId.replace(it.oldId, it.newId)
                }
                field.set(sourceClass, oldId)
            }
        }
    }
    
    fun changeOldRelId4NewData(data: Any, dataTransferHistories: MutableList<DataTransferHistory>, currentProjectId: String) {
        val fieldMap = CommonUtils.getEntityAllField(data)
        fieldMap.values.forEach { field ->
            if (field.type.toString().contains("java.lang.String") && null != field.get(data)) {
                var oldId = field.get(data).toString()
                dataTransferHistories.forEach {
                    oldId = oldId.replace(it.oldId, it.newId)
                }
                field.set(data, oldId)
            }
        }
        fieldMap["projectId"]!!.set(data, currentProjectId)
    }
    /**
     * 数据迁移记录表
     */
    @Entity
    @Table(uniqueConstraints = [UniqueConstraint(columnNames = ["oldId", "projectId"])])
    data class DataTransferHistory (
    
            var oldId: String = "",
            var newId: String = "",
            var projectId: String = "",
            var tableName: String = "",
            var createTime: Instant = Instant.now(),
            @Id
            @GenericGenerator(name = "idGenerator", strategy = "uuid")
            @GeneratedValue(generator = "idGenerator")
            var id: String = ""
    
    )

    到这里就结束了,以上思路仅供参考。

    小结

    一)、数据备份需要项目独立
    二)、通过项目ID 区分备份的数据是用来数据还原还是数据迁移
    三)、数据迁移过程中需要考虑数据重复导入的问题
    四)、数据迁移过程中需要重新绑定主外键的关联
    五)、第三和第四点可以通过记录数据迁移表做辅助
    六)、数据迁移过程尽量避免删除操作。避免对其他项目造成影响。

    posted @ 2019-07-10 18:30 ITDragon龙 阅读(...) 评论(...) 编辑 收藏
  • “两学一做”在山西——黄河新闻网 2019-07-13
  • 内地生报读香港高校本科人数持续下跌 2019-07-13
  • 【学习时刻】人民大学王义桅:金砖合作的“自信”与“自觉” 2019-07-12
  • 女子请“私家侦探”被骗3万 警方循线捣毁诈骗团伙 2019-07-11
  • 【学习时刻】北交大马院院长韩振峰:高校思想政治工作必须牢牢把握三大根本问题 2019-07-11
  • 全国“非遗”保护工作先进名单公布 2019-07-01
  • 紫光阁中共中央国家机关工作委员会 2019-06-25
  • 杭州控烟令修改草案拟允许室内设吸烟区,控烟专家:跌破眼镜 2019-06-25
  • 挪用近30万报纸征订款赌博 河南一报社聘用制干部获刑 2019-06-23
  • 2016年,有1145家上市公司大小非减持了3600亿元,还有210名上市公司高管减持了1400亿元。IPO已成了造就成千上万个十亿、百亿富豪的捷径, 2019-06-21
  • 专家“把脉”中国电影市场:提升品质方能逆袭 2019-06-21
  • “善款资助副局长儿子留学”真相须尽快落地 2019-06-19
  • 21岁女护士失联2天后确认遇害 嫌疑人为其前男友 2019-06-19
  • 中国地质公园名录旅行地中国国家地理网 2019-06-13
  • 玄关运用有四大原则 用的好才能财旺挡煞聚财 ——凤凰网房产 2019-06-10
  • 360彩票中心安全吗 高频彩上海时时乐下载 cba排名20192019 快中彩走势图 德州扑克的玩法技巧 内蒙古快三走势图360 彩票双色球开奖号是多少 意甲各球队球衣 14086期双色球开奖号码 娱乐场所厚白方巾 连码专家六码复式 云南时时彩快乐十分钟 安徽时时彩官网平台 牌九天地游戏机技巧 安徽时时彩官网