jvm的gc
redis的rehash
codis的rehash
业务系统数据迁移核心是update (无状态迁移,双写)
业务模型的重构和模型改造. 层级关系. 新建字段. 原来的还是写,但是不再读,和作业务判断. 同样也是双写的机制. 另外开一张表不合适,状态什么的可以不用新改. 不然后续更新也要双更新.
hashmap 的 resize
concurrentHashMap 的 rehash
要求:
1.可遍历.
1.按顺序遍历
2.不按顺序遍历
2.可拆解
两种模式.
1. 有状态.记录哪些迁移,哪些没迁移
2.无状态, insert 单向. 双读,双更新.
选择后种.
整体迁移时的diff log. 保存更改的log. stop the world. 迁移,切换.
改进点: 将整体迁移改成逐步迁移, 实现方案是 利用hash 后获得slot 加锁迁移,, 逐步迁移. 缺点: 降低吞吐量.
1. 单线程,无锁.增删改查,rehash迁移线程.
2. 修改增删改查逻辑. 当迁移时需要先迁移,再进行操作. 最小粒度原则.
3. 遍历所有的key.(通过hash槽位遍历)
redis的rehash,利用了双hash表。属于迁移问题.
rehash阶段的读操作 http://www.th7.cn/db/nosql/201602/176178.shtml rehash阶段改变了原来的读操作,含写。
1. 引入slot的概念,分层, zookeeper只记录slot集合信息(slot量可控), 如何遍历一个slot下所有的key ,一个key属于哪个slot有谁来记录?
Slot模型层,代码位置在pkg/models/slot.go。
原生Redis中并没有Slot这个概念,也就是说:虽然我们给Key分配好了Slot,但是一旦存入Redis后Key属于哪个Slot这个信息就丢失了。解决的方法有很多种,比如:
2.对于迁移中的Slot,如果恰好此时有客户端要访问该Slot中的某个Key该怎么办?Codis不是遇到这个问题的第一个中间件,像Taobao Tair (和jvm类似)中也有对此的解决方案:
发生迁移的时候data server如何对外提供服务?
当迁移发生的时候, 我们举个例子, 假设data server A 要把 桶 3,4,5 迁移给data server B. 因为迁移完成前, 客户端的路由表没有变化, 客户端对 3, 4, 5 的访问请求都会路由到A. 现在假设 3还没迁移, 4 正在迁移中, 5已经迁移完成. 那么如果是对3的访问, 则没什么特别, 跟以前一样. 如果是对5的访问, 则A会把该请求转发给B,并且将B的返回结果返回给客户, 如果是对4的访问, 在A处理, 同时如果是对4的修改操作, 会记录修改log.当桶4迁移完成的时候, 还要把log发送到B, 在B上应用这些log. 最终A B上对于桶4来说, 数据完全一致才是真正的迁移完成
但Codis采取的是不同的策略。当迁移过程中发生数据访问时,Proxy会发送”slotsmgrttagone”迁移命令给Redis,强制将客户端要访问的Key立刻迁移,然后再处理客户端的请求。(ps: 需记录某个key是否已经迁移(基于slot两级隔离, 所以这个key总体不会太大). 可不用加锁, 大不了重复迁移,ps: 每个redis实例需要记录某个key是否已迁移(codis改造部分))
func (m *CodisSlotMigrator) Migrate(slot *models.Slot, fromGroup, toGroup int, task *MigrateTask, onProgress func(SlotMigrateProgress)) (err error) {
groupFrom, err := models.GetGroup(task.zkConn, task.productName, fromGroup)
groupTo, err := models.GetGroup(task.zkConn, task.productName, toGroup)
fromMaster, err := groupFrom.Master(task.zkConn)
toMaster, err := groupTo.Master(task.zkConn)
c, err := redis.Dial("tcp", fromMaster.Addr)
defer c.Close()
_, remain, err := sendRedisMigrateCmd(c, slot.Id, toMaster.Addr)
for remain > 0 {
if task.Delay > 0 {
time.Sleep(time.Duration(task.Delay) * time.Millisecond)
}
_, remain, err = sendRedisMigrateCmd(c, slot.Id, toMaster.Addr)
if remain >= 0 {
onProgress(SlotMigrateProgress{
SlotId: slot.Id,
FromGroup: fromGroup,
ToGroup: toGroup,
Remain: remain,
})
}
}
return nil
}
func sendRedisMigrateCmd(c redis.Conn, slotId int, toAddr string) (int, int, error) {
addrParts := strings.Split(toAddr, ":")
if len(addrParts) != 2 {
return -1, -1, ErrInvalidAddr
}
reply, err := redis.Values(c.Do("SLOTSMGRTTAGSLOT", addrParts[0], addrParts[1], MIGRATE_TIMEOUT, slotId))
if err != nil {
return -1, -1, err
}
var succ, remain int
if _, err := redis.Scan(reply, &succ, &remain); err != nil {
return -1, -1, err
}
return succ, remain, nil
}
* hashmap 的 rehash 会修改原 entry 的实体, 调换链表顺序,故并发下会死循环(1.7和1.8的 resize 方案不一致). 遍历操作会 fail-over .resize 关注的是槽位,而不是已插入的节点数.
* concurrentHashMap的 resize 不会,entrySet 是不可变的,next 值不会变. rehash 时生成新的 entrySet .故 enteySet. iterator遍历操作不加锁. 可能拿到的 不同 segment 下的 hashtable[] 是不同的.
最好是记录每个id的迁移记录.
另外方案: 三级索引. 主开关,递增id号,新库初始id号.小与此都迁移过,大于此未迁移. (迁移开关一旦打开,老数据库中就不会新增数据).
各分布式定时任务.获取迁移序列锁.迁移指定序列的数据. 最后验证下数据总量是否一致.id范围+hash进行验证排查.
两表共同迁移方案: ?
读: 根据序号判断去哪里读
写: 根据开关写入哪个新库
删: 根据开关删除哪里.
改:根据开关和序号,和加锁情况去哪里修改.
http://blog.csdn.net/dc_726/article/details/47355989?ref=myread