IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> Go + Redis 实现分布式锁 -> 正文阅读

[大数据]Go + Redis 实现分布式锁

一、前言

1.1 需要对交易订单加锁原因

开始本篇文章分享之前,先简单进行一下项目描述。该项目为一个中心化钱包。java接收到用户的以太坊转账请求后,调用后端golang服务的转账接口,将交易发送至链上。

如果golang服务处理交易后,正常返回交易哈希至java,说明该交易已发送至链上,后续检查交易哈希是否上链即可。如果因为网络等原因,java未收到golang返回的交易哈希,则认为该交易出现问题,java应将该交易置为待处理状态,java不应该继续发送该订单交易,而等待人工介入,排查具体原因。从而防止用户双花。

在与java层同事沟通以上规则后,java层认为:交易失败后,不会进行重试,但如果代码错误导致bug出现或者交易出现并发的情况下(发送交易时应使用队列,不然一定会出现交易并发情况),可能会进行多笔同样订单交易发送,所以需要在后端golang这里进行加锁,进行最后一次拦截。

1.2 加锁方案

  1. levelDB或mysql 持久化存储

因为golang支持使用levelDB进行key值存储,所以可将java的交易订单利用levelDB进行持久化存储。已经接收的交易订单不再进行二次处理,如果需要重新发起这笔交易,由java改变交易订单后,重新发送。

因为后面考虑golang服务需要部署多节点负载,多节点的levelDB的存储可以使用共享存储,但levelDB的特点是一次只允许一个进程访问一个特定的数据库。所以不能作为分布式存储。

  1. redis

使用redis存储key值,实现简单的分布式锁。使用此种方式的缺点是:

  • key值存在过期时间,如果key值失效后,java层依然进行交易重试,则依然会出现双花现象,所以必须要在key值失效前,人工介入排查处理。
  • redis库如果被Flush,则key值不存在,问题交易会被放通
  • redis服务宕机,无法获取key值,同样也会出现交易双花问题

二、Go + Redis 实现分布式锁

2.1 为什么需要分布式锁

  1. 用户下单

锁住 uid,防止重复下单。

  1. 库存扣减

锁住库存,防止超卖。

  1. 余额扣减

锁住账户,防止并发操作。

分布式系统中共享同一个资源时往往需要分布式锁来保证变更资源一致性。

2.2 分布式锁需要具备特性

  1. 排他性

锁的基本特性,并且只能被第一个持有者持有。

  1. 防死锁

高并发场景下临界资源一旦发生死锁非常难以排查,通常可以通过设置超时时间到期自动释放锁来规避。

3.可重入

锁持有者支持可重入,防止锁持有者再次重入时锁被超时释放。

4.高性能高可用

锁是代码运行的关键前置节点,一旦不可用则业务直接就报故障了。高并发场景下,高性能高可用是基本要求。

2.3 实现 Redis 锁应先掌握哪些知识点

  1. set 命令
SET key value [EX seconds] [PX milliseconds] [NX|XX]
  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。

先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。

  1. setnx 方法

可以先来看一下setnx方法:

unc (c *cmdable) SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd {
	var cmd *BoolCmd
	if expiration == 0 {
		// Use old `SETNX` to support old Redis versions.
		cmd = NewBoolCmd("setnx", key, value)
	} else {
		if usePrecise(expiration) {
			cmd = NewBoolCmd("set", key, value, "px", formatMs(expiration), "nx")
		} else {
			cmd = NewBoolCmd("set", key, value, "ex", formatSec(expiration), "nx")
		}
	}
	c.process(cmd)
	return cmd
}

setnx的含义就是SET if Not Exists,该方法是原子的。如果key不存在,则设置当前key为value成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。

expire(key, seconds)
expire设置过期时间,要注意的是setnx命令不能设置key的超时时间,只能通过expire()来对key设置。

2.4 golang 连接redis

  • 下载reids包
go get github.com/go-redis/redis
  • golang连接redis
package redis

import (
	"github.com/go-redis/redis"
	"wallet/config"
	"wallet/pkg/logger"
)

// RedisDB Redis的DB对象
var RedisDB *redis.Client

func NewRedis() {  //创建redis连接
	RedisDB = redis.NewClient(&redis.Options{
		Addr:     config.Conf.Redis.Host,   //redis连接地址
		Password: config.Conf.Redis.Password,  //redis连接密码
		DB:       config.Conf.Redis.Database,  //redis连接库
	})

	defer func() {
		if r := recover(); r != nil {
			logger.Error("Redis connection error,", r)
		}
	}()
	_, err := RedisDB.Ping().Result()
	if err != nil {
		panic(err)
	}
	logger.Info("Redis connection successful!")
}
  • 项目启动时,应首先创建一个redis连接
func main() {
	//连接redis
	redis.NewRedis()
}

2.5 golang + redis实现分布式锁

  1. 利用redis的Set方法进行存key,简单实现一个加锁的方法
//判断当前订单是否已进行处理
isExist := redis.RedisDB.Exists(order)
//判断是否获取到key值,若获取到,则说明该交易订单已请求,向调用者返回报错
if isExist.Val() != 0 {
	apicommon.ReturnErrorResponse(ctx, 1, "Order transaction already exists, please check transaction", "")
	return
} else { //若未获取到,则说明暂未处理此笔交易订单,向redis中set此订单
	redis.RedisDB.Set(order, order, 86400*time.Second)
}
  1. 利用redis的setnx方法,实现分布式加锁
//判断当前订单是否已进行处理
bool := redis.RedisDB.SetNX(order, order, 24*time.Hour)
if bool.Val() { //SetNX只进行一次操作,若返回为true,则之前未处理该订单,此次已set该key
	logger.Info("The transaction order key value has been saved")
} else { //若返回false,则说明该交易订单已请求,向调用者返回报错
	logger.Error("The transaction order key value has been saved")
	apicommon.ReturnErrorResponse(ctx, 1, "Order transaction already exists, please check transaction", "")
	return
}

2.6 总结

通过代码和执行结果可以看到,我们远程调用 setnx 实际上和单机的 trylock 非常相似,如果获取锁失败,那么相关的任务逻辑就不应该继续向前执行。

setnx 很适合在高并发场景下,用来争抢一些“唯一”的资源。比如交易撮合系统中卖家发起订单,而多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不管是用户设备的时间,还是分布式场景下的各台机器的时间,都是没有办法在合并后保证正确的时序的。哪怕是我们同一个机房的集群,不同的机器的系统时间可能也会有细微的差别。

所以,我们需要依赖于这些请求到达 redis 节点的顺序来做正确的抢锁操作。如果用户的网络环境比较差,那也只能自求多福了。

参考:

  • golang操作redis官方文档:https://pkg.go.dev/github.com/go-redis/redis#Client.Expire
  • golang分布式锁: https://books.studygolang.com/advanced-go-programming-book/ch6-cloud/ch6-01-lock.html
  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-03-11 22:17:06  更:2022-03-11 22:21:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:07:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码