实时数据并发写入 Redis 优化

代码星冰乐

专注成就未来

首页 归档 关于

实时数据并发写入 Redis 优化

Nov 10, 2019 | haifeiWu | Java | 阅读
文章目录
  1. 1. 背景
  2. 2. 乐观锁实现数据的并发更新
  3. 3. 可能存在问题及其解决方案
  4. 4. 小结

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/a5e3/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。


由于版权原因,请阅读原文 --> 实时数据并发写入 Redis 优化

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/a5e3/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/a5e3/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

背景

当前架构的逻辑是将并发请求数据写入队列中,然后起一个单独的异步线程对数据进行串行处理。这种方式的好处就是不用考虑并发的问题,当然其弊端也是显而易见的~

乐观锁实现数据的并发更新

根据当前业务的数据更新在秒级,key 的碰撞率较低的情况。笔者打算采用使用 CAS 乐观锁方案:使用 Lua 脚本实现 Redis 对数据的原子更新,即便是在并发的情况下其性能也会上一个级别。下面是 CAS 乐观锁实现数据并发更新的流程图:
CAS

根据上面的流程图设计出了 Lua 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local keys,values=KEYS,ARGV
local version = redis.call('get',keys[1])
if values[1] == '' and version == false
then
redis.call('SET',keys[1],'1')
redis.call('SET',keys[2],values[2])
return 1
end

if version == values[1]
then
redis.call('SET',keys[2],values[2])
redis.call('INCR',keys[1])
return 1
else
return 0
end

可能存在问题及其解决方案

1,在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func main() {
for i := 0; i < 10; i++ {
isRetry := execLuaScript()
if !isRetry {
break
}
}
}

func execLuaScript() bool {
ctx := context.Background()
r := client.GetRedisKVClient(ctx)
defer r.Close()

luaScript := `
local keys,values=KEYS,ARGV
local version = redis.call('get',keys[1])
if values[1] == '' and version == false
then
redis.call('SET',keys[1],'1')
redis.call('SET',keys[2],values[2])
return 1
end

if version == values[1]
then
redis.call('SET',keys[2],values[2])
redis.call('INCR',keys[1])
return 1
else
return 0
end`

casVersion, err := r.Get("test_version")

kvs := make([]redis.KeyAndValue, 0)
kvs = append(kvs, redis.KeyAndValue{"test_version", casVersion.String()})
kvs = append(kvs, redis.KeyAndValue{"test", "123123123"})
mv, err := r.Eval(luaScript, kvs...)

if err != nil {
log.Errorf("%v", err)
}

val, _ := mv.Int64()
log.Debugf(">>>>>> lua 脚本运行结果 :%d", val)
if val == 1 {
// lua 脚本执行成功,无需重试
return false
} else if val == 0 {
return true
}
}

2,Lua 脚本执行时只能在同一台机器上生效,因此在 Redis 集群在就要求相关联的 key 分配到相同机器。这里很多同学可能会问为什么,其实很简单,Redis 是单线程的,倘若 Lua 脚本操作的 key 在不同机器上执行,也就无法保证其执行的原子性了。

解决方法还是从分片技术的原理上找:
数据分片,就是一个 hash 的过程:对 key 做 md5,sha1 等 hash 算法,根据 hash 值分配到不同的机器上。

为了实现将key分到相同机器,就需要相同的 hash 值,即相同的 key(改变 hash 算法也行,但比较复杂)。但 key 相同是不现实的,因为 key 都有不同的用途。但是我们让 key 的一部分相同对我们业务实现来说是可以实现的。那么能不能拿 key 一部分来计算 hash 呢?答案是肯定的,

这就是 Hash Tag 。允许用key的部分字符串来计算hash。当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做 hash。假设 hash 算法为sha1。对 user:{user1}:ids和user:{user1}:tweets ,其 hash 值都等同于 sha1(user1)。

小结

对于上面的优化过程,目前代码重构开发工作已经完成,但是还未正式上线,等上线之后再来补一下优化之后性能的提升情况~

关注我们

作 者:haifeiWu
原文链接:https://www.hchstudio.cn/article/2019/a5e3/
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

分享
Kafka Consumer 的 Rebalance 机制Redis 与 Lua 使用中的小问题
微信关注我们
分类
  • Android8
  • Go4
  • Java59
  • Kafka,Java1
  • Kotlin2
  • Linux1
  • MapReduce1
  • Python2
  • Raft1
  • Redis1
  • ThreadPoolExecutor1
  • go1
  • 工具1
  • 总结8
  • 旅游日记1
标签
Nginx ChanghuiN haifeiWu Android Java 设计模式 hexo Kotlin 算法 MySQL 源码解析 Python Redis golang web Kafka 配置中心 总结 性能优化 旅游日记 Shell Go 问题排查 译文 Docker Spring Boot 工具 学习笔记 WebFlux 性能测试 go 散列表 源码 netty Raft
最近文章
  • Kafka的日志复制机制
  • 从20到21
  • go 并发编程
  • 【译】了解Linux CPU负载-您何时应该担心?
  • Zookeeper 与分布式锁
  • 基于Redis的分布式锁到底安全吗?
  • 【译】Raft 学生指南
  • ThreadPoolExecutor 的简单梳理
  • MapReduce 的简单实现
  • 使用 Map 实现策略模式
福利专区
    免费SSL证书
      阿里云红包
        腾讯云专属福利
        Copyright © 2021 代码星冰乐. Powered by ChanghuiN. 版权所有 晋ICP备15001365号
        特别感谢: 云服务器服务商 、 CDN 服务商