1 压缩性:任意长度的数据,算出MD5值长度都是固定的
2 容易计算:从原数据计算出MD5值很容易
3 抗修改性: 对原数据进行修改任何修改,哪怕1个字节,MD5值差异很大
4 强碰撞:想找到两个不同的数据,使他们具有相同的MD5值,非常困难
5 不可逆性:不可反解
1:加盐:
1:通过生成随机数和MD5生成字符串进行组合
2:数据库同时存储MD5和salt(索特)值,验证正确性使用salt进行MD5即可
//生成盐值
//salt, encodedPwd := password.Encode("generic password", nil)
//fmt.Println(salt)
//fmt.Println(encodedPwd)
验证
//check := password.Verify("generic password", salt, encodedPwd, nil)
//fmt.Println(check) // true
// 设置盐值为多少,迭代多少次,key长度多少,md5
options := &password.Options{16, 100, 32, sha512.New}
salt, encodedPwd := password.Encode("generic password", options)
password1 := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)
fmt.Println(len(password1))
fmt.Println(salt)
fmt.Println(encodedPwd)
fmt.Println(password1)
passwordInfo := strings.Split(password1, "$")
fmt.Println(passwordInfo)
check := password.Verify("generic password", passwordInfo[2], passwordInfo[3], options)
fmt.Println(check) // true
https://github.com/uber-go/zap
go get -u go.uber.org/zap
package main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync() // flushes buffer, if any
url := "https://imooc.com"
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
// Structured context as loosely typed key-value pairs.
"url", url,
"attempt", 3,
)
sugar.Infof("Failed to fetch URL: %s", url)
}
Zap提供了两种类型的日志记录器— Sugared Logger 和 Logger 。
在性能很好但不是很关键的上下文中,使用 SugaredLogger 。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger 。它甚至比 SugaredLogger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录
package main
import (
"go.uber.org/zap"
"time"
)
func NewLogger() (*zap.Logger, error) {
cfg := zap.NewProductionConfig()
cfg.OutputPaths = []string{
"./myproject.log",
}
return cfg.Build()
}
func main() {
//logger, _ := zap.NewProduction()
logger, err := NewLogger()
if err != nil {
panic(err)
//panic("初始化logger失败")
}
su := logger.Sugar()
defer su.Sync()
url := "https://imooc.com"
su.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
}
Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。它支持以下特性:
https://www.runoob.com/w3cnote/yaml-intro.html
https://github.com/spf13/viper
go get github.com/spf13/viper
package main
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"time"
)
type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type ServerConfig struct{
Name string `mapstructure:"name"`
MysqlInfo MysqlConfig `mapstructure:"mysql"`
}
func GetEnvInfo(env string) string {
viper.AutomaticEnv()
return viper.GetString(env)
}
func main(){
data := GetEnvInfo("Debug")
var configFileName string
configFileNamePrefix := "config"
if data == "true" {
configFileName = fmt.Sprintf("viper_test/%s-debug.yaml", configFileNamePr
}else{
configFileName = fmt.Sprintf("viper_test/%s-pro.yaml", configFileNamePref
}
serverConfig := ServerConfig{}
fmt.Println(data)
v := viper.New()
v.SetConfigFile(configFileName)
err := v.ReadInConfig()
if err != nil {
panic(err)
}
if err := v.Unmarshal(&serverConfig); err != nil {
panic(err)
}
fmt.Println(serverConfig)
go func() {
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
_ = v.ReadInConfig() // 读取配置数据
_ = v.Unmarshal(&serverConfig)
fmt.Println(serverConfig)
})
}()
time.Sleep(time.Second*3000)
}
package main
import (
"fmt"
"github.com/spf13/viper"
)
type ServerConfig struct {
//底层使用mapstructure
ServiceName string `mapstructure:"name"`
Port int `mapstructure:"prot"`
}
func main() {
v := viper.New()
//文件的路径如何设置
v.SetConfigFile("viper_test/ch01/config.yaml")
if err := v.ReadInConfig(); err != nil {
panic(err)
}
serverConfig := ServerConfig{}
//映射
err := v.Unmarshal(&serverConfig)
if err != nil {
panic(err)
}
fmt.Println(serverConfig)
//fmt.Println(v.Get("name"))
fmt.Println(v.Get("name"))
}
案例源码
package main
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"time"
)
//如何将线上和线下开发配置隔离
//不用改任何代码而且线上和线下的配置文件能隔离开
type ServerConfig struct {
//底层使用mapstructure
ServiceName string `mapstructure:"name"`
MysqlInfo MysqlConfig `mapstructure:"mysql"`
}
type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
// GetEnvInfo 读取环境变量
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
return viper.GetBool(env)
//刚才设置的环境变量,想要生效,我们必须重启goland
}
func main() {
//fmt.Println(GetEnvInfo("CHENGPENG_DEBUG"))
//设置全局变量
debug := GetEnvInfo("CHENGPENG_DEBUG")
configFilePrefix := "config"
configFileName := fmt.Sprintf("viper_test/ch02/%s-pro.yaml", configFilePrefix)
if debug {
configFileName = fmt.Sprintf("viper_test/ch02/%s-debug.yaml", configFilePrefix)
}
v := viper.New()
//文件的路径如何设置
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
serverConfig := ServerConfig{}
//映射
err := v.Unmarshal(&serverConfig)
if err != nil {
panic(err)
}
fmt.Println(serverConfig)
//fmt.Println(v.Get("name"))
//fmt.Println(v.Get("name"))
//viper的功能,动态监控变量==>不会阻塞
v.WatchConfig()
v.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("config file channed", in.Name) //监控文件的变化
_ = v.ReadInConfig()
_ = v.Unmarshal(&serverConfig)
fmt.Println(serverConfig)
})
time.Sleep(time.Second * 300)
}
https://zh.mojotv.cn/go/refactor-base64-captcha
consul安装
docker run -d -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8302:8302 -p 8600:8600/udp consul consul agent -dev -client=0.0.0.0
https://developer.hashicorp.com/consul/api-docs/agent/service#register-service
https://developer.hashicorp.com/consul/api-docs/agent/service#deregister-service
https://developer.hashicorp.com/consul/api-docs/agent/check
https://github.com/mbobakov/grpc-consul-resolver
https://github.com/grpc/grpc/blob/master/doc/service_config.md
https://help.aliyun.com/zh/oss/developer-reference/go-installation
https://github.com/aliyun/aliyun-oss-go-sdk
https://opendocs.alipay.com/common/02kkv7
package main
import (
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"os"
)
func handleError(err error) {
fmt.Println("Error:", err)
os.Exit(-1)
}
func main() {
endpoint := "http://xxxxxx"
accessKeyId := "xxxxxxxxx"
accessKeySecret := "xxxxxxxx"
// yourBucketName填写存储空间名称。
bucketName := "xxxxx"
// yourObjectName填写Object完整路径,完整路径不包含Bucket名称。
objectName := "first.jpg"
// yourLocalFileName填写本地文件的完整路径。
localFileName := `C:\Users\Administrator\Desktop\yangyan.png`
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
//provider, err := oss.NewEnvironmentVariableCredentialsProvider()
//if err != nil {
// fmt.Println("Error:", err)
// os.Exit(-1)
//}
// 创建OSSClient实例。
// yourEndpoint填写Bucket对应的Endpoint,以华东1(杭州)为例,填写为https://oss-cn-hangzhou.aliyuncs.com。其它Region请按实际情况填写。
//client, err := oss.New(endpoint, accessKeyId, accessKeySecret, oss.SetCredentialsProvider(&provider))
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
fmt.Println("Error:", err)
os.Exit(-1)
}
// 获取存储空间。
bucket, err := client.Bucket(bucketName)
if err != nil {
handleError(err)
}
// 上传文件。
err = bucket.PutObjectFromFile(objectName, localFileName)
if err != nil {
handleError(err)
}
}
https://help.aliyun.com/zh/oss/use-cases/go-1#concept-mhj-zzt-2fb
https://help.aliyun.com/zh/oss/use-cases/obtain-signature-information-from-the-server-and-upload-data-to-oss
默认每个语句mysql都是默认提交的
行锁
明确查询条件只需锁住满足条件的数据-->只有在有索引的时候才会这样,如果你没有索引,那么行锁会升级成表锁
一般情况下-->锁只是锁住要更新的语句
尽量兼容高并发并不是将锁的并发降的最低
如果没有满足条件的结果,并不会锁表(索引字段)-->只有在有索引的时候才会这样,如果你没有索引,那么行锁会升级成表锁
https://gorm.io/zh_CN/docs/advanced_query.html
gorm实现悲观锁
// Sell 扣减库存 数据库加锁-->悲观锁-->行锁
func (InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*empty.Empty, error) {
//扣减库存 逐一一些商品进行扣减 本地事务 需要同时扣减成功或者同时扣减失败
//数据库基本的应用场景:数据一致性(数据库事务)
//并发情况之下,可能会出现超买
//开启事务
tx := global.DBMSQL.Begin()
//m.Lock() //获取锁 使用悲观锁就不要用互斥锁了
for _, goodInfo := range req.GoodsInfo {
var inv model.Inventory
//if result := global.DBMSQL.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
if result := tx.Clauses((clause.Locking{Strength: "UPDATE"})).Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 { //使用悲观锁 一旦协程拿到了update锁就进不来了==>这样就实现数据的一致性
tx.Rollback() //回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
}
//是否小于扣减数量
if inv.Stocks < goodInfo.Num {
tx.Rollback() //回滚之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
}
//真正扣减 会出现数据不一致的问题--使用分布式锁
inv.Stocks -= goodInfo.Num
tx.Save(&inv) //事务并没有真正提交到数据库
}
//提交事务 需要自己手动提交操作
tx.Commit()
return &empty.Empty{}, nil
}
// Reback 库存归还
// 1:订单超时归还 2订单创建失败 3手动归还
func (InventoryServer) Reback(ctx context.Context, req *proto.SellInfo) (*empty.Empty, error) {
//开启事务
tx := global.DBMSQL.Begin()
for _, goodInfo := range req.GoodsInfo {
var inv model.Inventory
if result := tx.Clauses((clause.Locking{Strength: "UPDATE"})).Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
tx.Rollback() //回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
}
//真正扣减 会出现数据不一致的问题--使用分布式锁
inv.Stocks += goodInfo.Num
tx.Save(&inv)
}
//提交事务 需要自己手动提交操作
tx.Commit()
return &empty.Empty{}, nil
}
乐观锁
1:没有让数据库加锁
2:不会出现数据不一致
优缺点
优点:
1. 简单
2. 不需要额外的组件 - 维护,mysql的维护比较简单 - 最合适的才是最好的。 系统的可用性
缺点:
性能
// Sell 乐观锁实现
func (InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*empty.Empty, error) {
//扣减库存 逐一一些商品进行扣减 本地事务 需要同时扣减成功或者同时扣减失败
//数据库基本的应用场景:数据一致性(数据库事务)
//并发情况之下,可能会出现超买
//开启事务
tx := global.DBMSQL.Begin()
//m.Lock() //获取锁 使用悲观锁就不要用互斥锁了
//var test []*int32
//var goodInfotest[]
for _, goodInfo := range req.GoodsInfo {
var inv model.Inventory
for {
if result := global.DBMSQL.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 { //使用悲观锁 一旦协程拿到了update锁就进不来了==>这样就实现数据的一致性
tx.Rollback() //回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
}
//test = &inv.ID
//是否小于扣减数量
if inv.Stocks < goodInfo.Num {
tx.Rollback() //回滚之前的操作
return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
}
//真正扣减 会出现数据不一致的问题--使用分布式锁
inv.Stocks -= goodInfo.Num
//根据条件更新
//update inventory set stocks=stocks-1,version=version+1 where goods=goodsId and version=version
//这种写法有瑕疵 Gorm不会更新零值 这种会被gorm给忽略掉
//if result := tx.Model(&model.Inventory{}).Where("goods = ? and version =?", goodInfo.GoodsId, inv.Version).Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version + 1}); result.RowsAffected == 0 {
// zap.S().Info("库存扣减失败")
//} else {
// break
//}
//强制更新零值
if result := tx.Model(&model.Inventory{}).Select("Stocks", "Version").Where("goods = ? and version =?", goodInfo.GoodsId, inv.Version).Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version + 1}); result.RowsAffected == 0 {
zap.S().Info("库存扣减失败")
} else {
break
}
}
}
//提交事务 需要自己手动提交操作
tx.Commit()
//最后把版本号更新正确版本
for _, info := range req.GoodsInfo {
//var inv model.Inventory
global.DBMSQL.Select("Version").Where("goods = ?", info.GoodsId).Updates(model.Inventory{Version: 0})
}
return &empty.Empty{}, nil
}
// Reback 库存归还
// 1:订单超时归还 2订单创建失败 3手动归还
func (InventoryServer) Reback(ctx context.Context, req *proto.SellInfo) (*empty.Empty, error) {
//开启事务
tx := global.DBMSQL.Begin()
for _, goodInfo := range req.GoodsInfo {
var inv model.Inventory
for {
if result := tx.Clauses((clause.Locking{Strength: "UPDATE"})).Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
tx.Rollback() //回滚之前的操作
return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
}
//真正扣减 会出现数据不一致的问题--使用分布式锁
inv.Stocks += goodInfo.Num
if result := tx.Model(&model.Inventory{}).Select("Stocks", "Version").Where("goods = ? and version =?", goodInfo.GoodsId, inv.Version).Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version + 1}); result.RowsAffected == 0 {
zap.S().Info("库存扣减失败")
} else {
break
}
}
}
//提交事务 需要自己手动提交操作
tx.Commit()
//最后把版本号更新正确版本
for _, info := range req.GoodsInfo {
//var inv model.Inventory
global.DBMSQL.Select("Version").Where("goods = ?", info.GoodsId).Updates(model.Inventory{Version: 0})
}
return &empty.Empty{}, nil
}
https://github.com/go-redsync/redsync
分布式锁需要解决的问题:
互斥性:任意时刻只能有一个客户端拥有锁,不能同时多个客户端获取
安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除
死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避
免该类问题的发生
1 setnx的作用
2. 如果我的服务挂掉了- 死锁
2. 设置过期时间
3. 如果你设置了过期时间,那么如果过期时间到了我的业务逻辑没有执行完怎么办?
1 在过期之前刷新一下
2 需要自己去启动协程完成延时的工作
3 延时的接口可能会带来负面影响 - 如果其中某一个服务hung住了, 2s就能执行完,但是你hung住那么你就会一直去申请延长锁,导致别人永远获取不到锁,这个很要命
3. 分布锁需要解决的问题 - lua脚本去做
1.互斥性 - setnx
2. 死锁
3. 安全性
安全性. 锁只能被持有该锁的用户删除,不能被其他用户删除
1. 当时设置的value值是多少只有当时的g(人)才能知道
2. 在删除的时取出redis中的值和当前自己保存下来的值对比一下
3. 即使你这样实现了分布式但是还是会有问题 - redlock(红锁)
如果主宕机或者网络故障的时候,那么其他的redis会找到一个redis当主机,其他当从机,就出现一个问题,此时setnx还没有同步到setnx上,库存服务2就去redis3里面去setnx,那么就会出现问题—>此时就产生了一个名词红锁
setnx操作应该在多台服务上进行–>就不需要关注同步的问题
谁先拿到多数谁就会成功就解决数据同步的问题
会通过协程异步去拿服务器,如果看是否能拿到,拿到多数服务器是否为拿到多少。拿锁的时候会消耗时间片,还有当前的时钟的时间==>过期时间还有多少
假设有5个完全独立的redis主服务器
1.获取当前时间戳
2.client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
3.client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功
4.如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁
算法示意图如下:
如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
RedLock算法是否是异步算法?
可以看成是同步算法;因为 即使进程间(多个电脑间)没有同步时钟,但是每个进程时间流速大致相同;并且时钟漂移相对于TTL叫小,可以忽略,所以可以看成同步算法;(不够严谨,算法上要算上时钟漂移,因为如果两个电脑在地球两端,则时钟漂移非常大)
RedLock失败重试
当client不能获取锁时,应该在随机时间后重试获取锁;并且最好在同一时刻并发的把set命令发送给所有redis实例;而且对于已经获取锁的client在完成任务后要及时释放锁,这是为了节省时间;
RedLock释放锁
由于释放锁时会判断这个锁的value是不是自己设置的,如果是才删除;所以在释放锁时非常简单,只要向所有实例都发出释放锁的命令,不用考虑能否成功释放锁;
RedLock注意点(Safety arguments):
1.先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移;
2.对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况, 从而使锁失效
3.一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效
系统有活性的三个特征
1.能够自动释放锁
2.在获取锁失败(不到一半以上),或任务完成后 能够自动释放锁,不用等到其自动过期
3.在client重试获取哦锁前(第一次失败到第二次重试时间间隔)大于第一次获取锁消耗的时间;
4.重试获取锁要有一定次数限制
RedLock性能及崩溃恢复的相关解决方法
1.如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;
2.如果启动AOF永久化存储,事情会好些, 举例:当我们重启redis后,由于redis过期机制是按照unix时间戳走的,所以在重启后,然后会按照规定的时间过期,不影响业务;但是由于AOF同步到磁盘的方式默认是每秒-次,如果在一秒内断电,会导致数据丢失,立即重启会造成锁互斥性失效;但如果同步磁盘方式使用Always(每一个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能方面要有所取舍;
3.有效解决既保证锁完全有效性及性能高效及即使断电情况的方法是redis同步到磁盘方式保持默认的每秒,在redis无论因为什么原因停掉后要等待TTL时间后再重启(学名:延迟重启) ;缺点是 在TTL时间内服务相当于暂停状态;
总结:
1.TTL时长 要大于正常业务执行的时间+获取所有redis服务消耗时间+时钟漂移
2.获取redis所有服务消耗时间要 远小于TTL时间,并且获取成功的锁个数要 在总数的一般以上:N/2+1
3.尝试获取每个redis实例锁时的时间要 远小于TTL时间
4.尝试获取所有锁失败后 重新尝试一定要有一定次数限制
5.在redis崩溃后(无论一个还是所有),要延迟TTL时间重启redis
6.在实现多redis节点时要结合单节点分布式锁算法 共同实现