Go语言实现Redis分布式锁2

项目地址: https://github.com/liwook/Redislock

1.支持阻塞式等待获取锁

之前的是只尝试获取一次锁,要是获取失败就不再尝试了。现在修改为支持阻塞式等待获取锁。

添加LockOptions结构体

添加option.go文件。

在LockOptions中

  • isBlock表示是否是阻塞模式
  • blockWaitingTime是获取key的阻塞超时时间
  • expire表示key的过期时间(之前是在结构体RedisLock,现在保存在LockOptions中)

const (DefaultExpireTime = 20 * time.SecondDefaultBlockWaitingTime = 8 * time.Second
)type LockOptions struct {isBlock          boolblockWaitingTime time.Durationexpire           time.Duration
}

 下面是LockOption的设置方法。

//option.go
type LockOptionFunc func(*LockOptions)// 设置阻塞等待
func WithBlock() LockOptionFunc {return func(option *LockOptions) {option.isBlock = true}
}//设置阻塞等待时间的上限
func WithBlockWaiting(waiting time.Duration) LockOptionFunc {return func(option *LockOptions) {option.blockWaitingTime = waiting}
}//设置续期的时长,也是key过期的时长
func WithExpire(exprie time.Duration) LockOptionFunc {return func(option *LockOptions) {option.expire = exprie}
}func setLock(o *LockOptions) {if o.isBlock && o.blockWaitingTime <= 0 {//默认阻塞等待时间上限是8o.blockWaitingTime = 8}if o.expire == 0 {o.expire = DefaultExpireTime}
}

修改创建锁的代码

//该结构体添加了LockOptions,去掉了expire成员
type RedisLock struct {LockOptionskey      stringId       string //锁的标识redisCli *redis.Client
}func NewRedisLock(cli *redis.Client, key string, opts ...LockOptionFunc) *RedisLock {id := strings.Join(strings.Split(uuid.New().String(), "-"), "")lock := RedisLock{key:      key,Id:       id,redisCli: cli,}//执行一些配置操作for _, optFunc := range opts {optFunc(&lock.LockOptions)}setLock(&lock.LockOptions)return &lock
}//用法lock := redislock.NewRedisLock(client, key, redislock.WithBlock(), redislock.WithBlockWaiting(10*time.Second))//之前的写法
// func NewRedisLock(cli *redis.Client, key string) *RedisLock {
// 	id := strings.Join(strings.Split(uuid.New().String(), "-"), "")
// 	return &RedisLock{
// 		key:      key,
// 		expire:   defaultExpireTime,
// 		Id:       id,
// 		redisCli: cli,
// 	}
// }

加锁

加锁主要分成了3步:

  1. 不管是不是阻塞的,都先尝试获取一次锁tryLock()
  2. 非阻塞加锁失败的话,就直接返回错误
  3. 之后基于阻塞模式轮询去获取锁
func (lock *RedisLock) Lock() (bool, error) {//不管是否是阻塞的,都是要先获取一次锁success, err := lock.tryLock()if success && err == nil {return success, err}//非阻塞加锁失败的话,直接返回错误if !lock.isBlock {return false, err}//基于阻塞模式轮询去获取锁return lock.blockingLock()
}func (lock *RedisLock) tryLock() (bool, error) {return lock.redisCli.SetNX(lock.key, lock.Id, lock.expire).Result()
}

  阻塞模式中使用了定时器轮询去获取锁。

func (lock *RedisLock) blockingLock() (bool, error) {timeoutCh := time.After(lock.blockWaitingTime)//轮询ticker,定时器, 100ms循环一次去获取锁ticker := time.NewTicker(100 * time.Millisecond)defer ticker.Stop()for {select {case <-timeoutCh:return false, fmt.Errorf("block waiting timeout,err:%w", ErrLockAcquiredByOthers)case <-ticker.C:success, err := lock.tryLock() //尝试获取锁if success && err == nil {return success, nil}}}
}

测试使用

这样lock先后顺序可以获得锁了。

func main() {testBlockingLock()
}func testBlockingLock() {client := NewClient()defer client.Close()val, _ := client.Ping().Result()fmt.Println(val)key := "blockLock"lock := redislock.NewRedisLock(client, key, redislock.WithBlock(), redislock.WithBlockWaiting(15*time.Second))var wg sync.WaitGroupwg.Add(1)go func() {//尝试获取锁if success, err := lock.Lock(); success && err == nil {fmt.Println("go BLOCKlock get..")time.Sleep(4 * time.Second)lock.Unlock()}wg.Done()}()//尝试获取锁if success, err := lock.Lock(); success && err == nil {fmt.Println("BLOCKlock get...")time.Sleep(7 * time.Second)lock.Unlock()}wg.Wait()
}

2.锁续期的看门狗实现

这里仍然存在一个问题:当锁的持有者任务未完成,但是锁的有效期已过,虽然持有者此时仍可以完成任务,并且也不会误删其他持有者的锁,但是此时可能会存在多个执行者同时执行临界区代码,使得数据的一致性难以保证,造成意外的后果,分布式锁就失去了意义。

因此,需要一个锁的自动续期机制,分布式锁框架Redission中就有这么一个看门狗,专门为将要到期的锁进行续期。这里我们也来实现一个简单的看门狗。

在LockOptions添加关于锁续期和看门狗标识

const (// 默认的分布式锁过期时间,也是默认的续期时长DefaultExpireTime = 20 * time.Second// 看门狗工作时间间隙WatchDogWorkStepTime    = 10 * time.Second..........
)type LockOptions struct {................//强调,expire是key的过期时长,也是要进行续期时的续期时长expire           time.DurationwathchDogMode      bool
}

 下面是关于锁续期和看门狗标识的设置方法。

//设置续期的时长,也是key过期的时长,(在支持阻塞式等待获取锁的时候已展示过)
func WithExpire(exprie time.Duration) LockOptionFunc {return func(option *LockOptions) {option.expire = exprie}
}func setLock(o *LockOptions) {if o.isBlock && o.blockWaitingTime <= 0 {//没有设置默认阻塞时间就使用默认阻塞时长o.blockWaitingTime = DefaultBlockWaitingTime}if o.watchDogWorkStepTime == 0 {o.watchDogWorkStepTime = DefaultWatchDogWorkStepTime}//简单起见,就设置是开启看门狗模式o.wathchDogMode = trueif o.expire == 0 {o.expire = DefaultExpireTime}//比较续期时长和看门狗工作时间间隔if o.expire <= o.watchDogWorkStepTime {o.watchDogWorkStepTime = o.expire - 2}
}

添加watchDog方法

在watchDog内部开启新协程执行runWatchDog。把context.WithCancel的结果赋值给结构体RedisLock的stopDog,到时解锁的时候就可以调用RedisLock.stopDog就可以停止看门狗,回收看门狗协程。协程中调用runWatchDog方法。

type RedisLock struct {....................// 停止看门狗stopDog context.CancelFunc //通过context.CancelFunc去停止看门狗
}func (lock *RedisLock) watchDog() {if !lock.wathchDogMode {return}var ctx context.Contextctx, lock.stopDog = context.WithCancel(context.Background())//启动看门狗go func() {lock.runWatchDog(ctx)}()
}

runWatchDog方法中使用了go语言标准库中的Ticker实现定时查看锁是否过期。

在select 语句中,每隔WatchDogWorkStepTime秒就会触发一次 ticker进行续期,将key的过期时间重置。注意,这里也是使用Lua脚本封装了确认锁与锁续期的操作来用于原子化,以防止误续期了其他持有者的锁。

func (lock *RedisLock) runWatchDog(ctx context.Context) error {//开启一个定时器ticker := time.NewTicker(lock.watchDogWorkStepTime)defer ticker.Stop()script := redis.NewScript(LauCheckThenExpire)for {select {case <-ticker.C:result, err := script.Run(lock.redisCli, []string{lock.key}, lock.Id, lock.expire+3).Result()if err != nil {return err}if ret, _ := result.(int64); ret != 1 {return errors.New("can not expire lock without ownership of lock")}case <-ctx.Done():return nil}}
}

加锁时刻

相比起之前的,主要是添加了开头的defer函数。只要最终是获取了锁,就执行watchDog()。

func (lock *RedisLock) Lock() (success bool, err error) {defer func() {if success && err == nil {lock.watchDog()}}()//不管是否是阻塞的,都是要先获取一次锁success, err = lock.tryLock()if success && err == nil {return success, err}//非阻塞加锁失败的话,直接返回错误if !lock.isBlock {return false, err}//基于阻塞模式轮询去获取锁success, err = lock.blockingLock()return
}

 解锁时刻

相比之前的,也是添加了defer函数。这里就是用lock.stopDog()来停止看门狗,也规避潜在的协程泄漏问题.

func (lock *RedisLock) Unlock() error {defer func() {//停止看门狗if lock.stopDog != nil {lock.stopDog()}}()script := redis.NewScript(LauCheckAndDelete)result, err := script.Run(lock.redisCli, []string{lock.key}, lock.Id).Int64()if err != nil {return err}if result != 1 {return errors.New("can not unlock without ownership of lock")}return nil
}

3.RedLock实现

为什么需要RedLock

redis 的容错机制:为避免单点故障引起数据丢失问题,redis 会基于主从复制的方式实现数据备份增加服务的容错性.

以哨兵机制为例,哨兵会持续监听 master 节点的健康状况,倘若 master 节点发生故障,哨兵会负责扶持 slave 节点上位成为 master,以保证整个集群能够正常对外提供服务。

在分布式系统存在一个经典的 CAP 理论。

  • C:consistency,一致性

  • A:availability,可用性

  • P:Partition tolerance,分区容错性

 redis 走的是 AP 路线,为了保证服务的可用性和吞吐量,redis 在进行数据的主从同步时,采用的是异步执行机制

我们试想一种场景:

  • 时刻1:使用方 A 在 redis master 节点加锁成功,完成了锁数据的写入操作

  • 时刻2:redis master 宕机了,锁数据还没来得及同步到 slave 节点

  • 时刻3:未同步到锁数据的 slave 节点被哨兵升级为新的 master

  • 时刻4:使用方 B 前来取锁,由于新 master 中确实锁数据,所以使用方 B 加锁成功

这个时候可以使用redis红锁(redlock,全称 redis distribution lock)。redLock 的策略是通过增加锁的数量并基于多数派准则来解决这个问题

保证在 RedLock 下所有 redis 节点中达到半数以上节点可用时,整个红锁就能够正常提供服务。

规则的具体细节:

  • 获取当前的时间(毫秒)
  • 使用相同的key和随机值在N个master上获取锁。这里获取锁的尝试时间要远远小于锁的超时时间,是为了防止某个master挂了之后我们还在不停获取锁,导致被阻塞时间过长。比如:该锁20s过期,三个节点加锁花了21秒,那就是加锁失败。
  • 在大多数master上获取到了锁,并且中的获取时间小于锁的过期时间的情况下,才会被认为锁获取成功。
  • 如果锁获取成功,那锁的超时时间 = 最初的锁超时时间 - 获取锁的总耗时时间。
  • 如果锁获取失败,不管是因为获取成功的master的个数没有过半,还是因为获取锁的耗时超过了锁的过期时间,都会将已经设置了该key的master上的把该key删除。

 添加关于红锁的Option和结构体RedLock

添加结构体RedLockOptions。其内包括了单个节点的请求耗时的超时时间singleNodeTimeout和整个红锁的过期时间。

//option.go
//红锁的操作
type RedLockOptionFunc func(*RedLockOptions)type RedLockOptions struct {singleNodeTimeout time.Duration //单个节点的请求耗时的超时时间exprie            time.Duration //整个红锁的过期时间
}func WithSingleNodeTimeout(singleNodeTimeout time.Duration) RedLockOptionFunc {return func(opt *RedLockOptions) {opt.singleNodeTimeout = singleNodeTimeout}
}func WithRedLockExpire(expire time.Duration) RedLockOptionFunc {return func(opt *RedLockOptions) {opt.exprie = expire}
}func setRedLock(opt *RedLockOptions) {if opt.singleNodeTimeout <= 0 {opt.singleNodeTimeout = DefaultSingleLockTimeout}if opt.exprie <= 0 {opt.exprie = DefaultExpireTime}
}

新添redlock.go文件。添加结构体RedLock。

其是对多个节点进行加锁,锁的数量会增多,所以 RedLock中会存有*RedisLock的数组,还有RedLock的一些选项配置。

//redlock.go
//单个节点的请求锁的耗时时间上限
const DefaultSingleLockTimeout = 50 * time.Millisecondtype RedLock struct {locks []*RedisLockRedLockOptions
}

创建RedLock

因为是多个节点了,就会有多个节点的client,可以在option.go文件中创建结构体SingleNode,其中存有redis的地址和密码。

//option.go
type SingleNode struct {Address  string    //redis的地址Password string    //redis的密码
}

创建红锁主要分成4个步骤:

  1. 判断节点的个数,小于3个无意义
  2. 进行红锁的配置设置option
  3. 判断所有节点累计的加锁超时时间是否小于设定的分布式锁过期时间的1/10,这点是对应 获取锁的尝试时间要远远小于锁的超时时间。(不一定要1/10,可以自己设置)
  4. 对所有节点进行连接,并创建每个节点的redislock,并赋值给红锁的成员locks
func NewRedLock(key string, nodes []*SingleNode, opts ...RedLockOptionFunc) (*RedLock, error) {//步骤1 ,节点个数<3,没有意义if len(nodes) < 3 {return nil, errors.New("the number of node is less than 3")}//步骤2lock := RedLock{}for _, opt := range opts {opt(&lock.RedLockOptions)}setRedLock(&lock.RedLockOptions)//步骤3if lock.exprie > 0 && time.Duration(len(nodes))*lock.singleNodeTimeout*10 > lock.exprie {// 要求所有节点累计的超时阈值要小于分布式锁过期时间的十分之一return nil, errors.New("expire thresholds of single node is too long")}//步骤4lock.locks = make([]*RedisLock, 0, len(nodes))for _, node := range nodes {client := redis.NewClient(&redis.Options{Addr:     node.Address,Password: node.Password,})lock.locks = append(lock.locks, NewRedisLock(client, key, WithExpireSeconds(lock.exprie)))}return &lock, nil
}

加锁

对每个node进行加锁。并且对在singleNodeTimeout耗时时间内的加锁成功的锁进行计数。

要是加锁成功的个数超过一半,那即是加锁成功。

func (r *RedLock) Lock() (bool, error) {//成功加锁的个数successNum := 0//对每个node尝试加锁for _, lock := range r.locks {startTime := time.Now()success, err := lock.Lock()cost := time.Since(startTime)if err == nil && success && cost <= r.singleNodeTimeout {successNum++}}if successNum < (len(r.locks)>>1)+1 {return false, errors.New("lock failed,lock nodes are Not enough for half")}return true, nil
}

解锁

需要对所有节点进行解锁。其解锁是使用了(RedisLock).Unlock()。

// 解锁,需对所有节点解锁
func (r *RedLock) Unlock() error {var allErr errorfor _, lock := range r.locks {if err := lock.Unlock(); err != nil {if allErr == nil {allErr = err}}}return allErr
}

测试使用

func main() {testReadLock()
}func testReadLock() {nodes := getNodes()key := "redLock"redLock, err := redislock.NewRedLock(key, nodes, redislock.WithRedLockExpire(10*time.Second), redislock.WithSingleNodeTimeout(100*time.Millisecond))if err != nil {return}var wg sync.WaitGroupwg.Add(1)go func() {//lock1尝试获取锁if success, err := redLock.Lock(); success && err == nil {fmt.Println("go redLock get..")time.Sleep(4 * time.Second)redLock.Unlock()}wg.Done()}()//lock2尝试获取锁if success, err := redLock.Lock(); success && err == nil {fmt.Println("redLock get...")time.Sleep(7 * time.Second)redLock.Unlock()}wg.Wait()
}func getNodes() []*redislock.SingleNode {//三个节点addr1 := "127.0.0.1:10000"passwd1 := "okredis"addr2 := "127.0.0.1:10001"passwd2 := "okredis"addr3 := "127.0.0.1:10002"passwd3 := "okredis"return []*redislock.SingleNode{{Address:  addr1,Password: passwd1,},{Address:  addr2,Password: passwd2,},{Address:  addr3,Password: passwd3,},}
}

还是会存在的问题

在5台机器中(都是master),在代码中依次对这5台机器去加锁,只有成功的机器数大于一半就算加锁成功,其他机器也就没必要再去操作了,相反,如果大于一半的机器失败了,就算失败,其他机器也就没必要再去操作了。

 这时一样会出问题。

  • 线程A要加锁,对1,2,3,4,5这5个实例进行加锁。1,2,3成功,4,5加锁超时,那这时有三个master加锁成功,已超过一半,即是最终加锁成功了。
  • 而这时节点3挂了。很快运维人员把一个新节点顶替已挂的节点3。
  • 在新节点还没有该锁key时候,线程B来获取该锁,这时节点3,4,5就获取锁成功,也因为成功个数超过一半,也即是获取锁成功。这时就有两个线程同时获取同一把锁。

所以说红锁也是不能完全解决所有问题的。 

 Redis 官网关于红锁的描述,你能看到著名的关于红锁的神仙打架事件。即 Martin Kleppmann 和 Antirez 的 RedLock 辩论。一个是很有资历的分布式架构师,一个是 Redis 之父。

所以,使用红锁还是需要慎重。而且本文章实现的红锁是比较简单的,还有很多细节没有考虑到的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/799225.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

配置vscode用于STM32编译,Debug,github上传拉取

配置环境参考&#xff1a; Docs 用cubemx配置工程文件&#xff0c;用VScode打开工程文件。 编译的时候会有如下报错&#xff1a; vscode出现process_begin :CreateProcess failed 系统找不到指定文件 解决方案&#xff1a;在你的makefile中加上SHELLcmd.exe就可以了 参考…

java发送请求-cookie有关代码

在初始化后添加cookie的代码 用这个httpclients类调custom方法&#xff0c;进行代码定制化 找和cookie有关的方法&#xff0c;设置默认的cookie存储信息 入参是接口 将入参粘贴后找方法&#xff0c;用new实现这个接口 这个方法是无参空构造&#xff0c;可以使用 设置了cookie …

【C++】RapidJSON 设置支持 std::string,防止编译报错

问题 rapidjson 创建 json 数据&#xff0c;使用 std::string 字符串进行赋值&#xff0c;编译时&#xff0c;抱一堆错误 .... rapidjson/include/rapidjson/document.h:690:5: note: candidate expects 0 arguments, 1 provided [build] make[2]: *** [main/CMakeFiles/ma…

Betaflight 4.5RC3 AT32F435遇到的一些“怪”现象

Betaflight 4.5RC3 AT32F435遇到的一些“怪”现象 1. 源由2. “怪”现象2.1 电机#4没有RPM转速2.2 遥控器通道10接收机测试失败2.3 OSD 异常2.4 磁力计数据无法获取 3. 参考资料 1. 源由 升级下固件&#xff0c;追下“时髦”&#xff0c;赶下“潮流”&#xff0c;本着“活着就…

MySQL学习路线一条龙

引言 在当前的IT行业&#xff0c;无论是校园招聘还是社会招聘&#xff0c;MySQL的重要性不言而喻。 面试过程中&#xff0c;MySQL相关的问题经常出现&#xff0c;这不仅因为它是最流行的关系型数据库之一&#xff0c;而且在日常的软件开发中&#xff0c;MySQL的应用广泛&#…

蚁剑修改特征性信息

前言 我们首先得知道蚁剑的流量特征&#xff1a; 编码器和解码器的特征&#xff1a;蚁剑自带的编码器和解码器具有明显的特点&#xff0c;可以通过更改配置文件来达到流量加密的目的1。例如&#xff0c;蚁剑支持多种编码方式&#xff0c;如base64、chr、rot13等&#xff0c;这…

LLM 构建Data Multi-Agents 赋能数据分析平台的实践之②:数据治理之二(自动处理)

前述 在前文的multi Agents for Data Analysis的设计说起&#xff0c;本文将继续探索和测试借助llm实现基于私有知识库的数据治理全自动化及智能化。整体设计如下&#xff1a; 整个体系设计了3个Agent以及一个Planer&Execute Agent&#xff0c;第一个Agent用于从企业数据…

【Linux系列】“dev-node1“ 运行的操作系统分析

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Vue3与TypeScript中动态加载图片资源的解决之道

在前端开发中&#xff0c;Vue.js已成为一个备受欢迎的框架&#xff0c;尤其是在构建单页面应用时。Vue3的发布更是带来了许多性能优化和新特性&#xff0c;而TypeScript的加入则进一步提升了代码的可维护性和健壮性。然而&#xff0c;在实际的项目开发中&#xff0c;我们有时会…

牛客NC93 设计LRU缓存结构【hard 链表,Map Java】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/5dfded165916435d9defb053c63f1e84 思路 双向链表map最新的数据放头结点&#xff0c;尾节点放最老的数据&#xff0c;没次移除尾巴节点本地考察链表的新增&#xff0c;删除&#xff0c;移动节点参考答案Java im…

第六篇: 3.5 性能效果 (Performance)- IAB/MRC及《增强现实广告效果测量指南1.0》

​​​​​​​ 翻译计划 第一篇概述—IAB与MRC及《增强现实广告效果测量指南》之目录、适用范围及术语第二篇 广告效果测量定义和其他矩阵之- 3.1 广告印象&#xff08;AD Impression&#xff09;第三篇 广告效果测量定义和其他矩阵之- 3.2 可见性 &#xff08;Viewability…

正确使用@Autowired

目录 一、前言二、跟着官方文档&#xff0c;学习正确使用Autowired0、实验环境1、通过构造方法进行注入1.1 问题1&#xff1a;那万一没有这个CustomerPreferenceDao对象&#xff0c;会报错吗&#xff1f; 2、通过setter方法注入3、通过方法注入&#xff08;这个方法可以是任意名…

【Android】apk安装报错:包含病毒: a.gray.BulimiaTGen.f

​ 有时候apk安装或者更新时&#xff0c;显示&#xff1a;[高风险]包含病毒: a.gray.BulimiaTGen.f这种bug&#xff1b; 原因&#xff1a;这是手机管家误报病毒。 处理方法&#xff1a;我看网上其他资料可以进行申诉&#xff0c;也可以进行apk加固&#xff0c;我这边尝试用360…

无参数绕过RCE

一.什么是无参数 顾名思义&#xff0c;就是只使用函数&#xff0c;且函数不能带有参数&#xff0c;这里有种种限制&#xff1a;比如我们选择的函数必须能接受其括号内函数的返回值&#xff1b;使用的函数规定必须参数为空或者为一个参数等 无参数题目特征 if(; preg_replace…

基于小程序+ssm实现的悬赏信息发布系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;ssm 【…

2024年妈妈杯数学建模MathorCup数学建模思路B题思路解析+参考成品

1 赛题思路 (赛题出来以后第一时间在群内分享&#xff0c;点击下方群名片即可加群) 2 比赛日期和时间 报名截止时间&#xff1a;2024年4月11日&#xff08;周四&#xff09;12:00 比赛开始时间&#xff1a;2024年4月12日&#xff08;周五&#xff09;8:00 比赛结束时间&…

数字人解决方案——Champ单个视频单张图像生成可控且一致的人体视频生成

概述 Champ是阿里巴巴集团、南京大学和复旦大学的研究团队共同提出了一种创新的人体动画生成技术&#xff0c;Champ能够在仅有一段原始视频和一张静态图片的情况下&#xff0c;激活图片中的人物&#xff0c;使其按照视频中的动作进行动态表现&#xff0c;极大地促进了虚拟主播…

【Emgu CV教程】10.12、Moments()函数计算轮廓矩和质心

文章目录 一、概念介绍1.矩2.矩能干什么3.矩函数 二、演示1.原始素材2.代码3.运行结果 一、概念介绍 1.矩 矩&#xff0c;英文叫moment&#xff0c;是一个数学中的概念&#xff0c;以下的解释来自百度百科&#xff1a; 是不是看不懂&#xff0c;没关系&#xff0c;数学基础不…

mysqldump: Got error: 1049: Unknown database ‘root‘ when selecting the datab

1.问题描述 MySQL版本号&#xff1a;MySQL Server 8.3MySQL持久化到处数据库结构及数据 mysqldump: Got error: 1049: Unknown database root when selecting the datab2.问题解决 cmd 切换本地路径 cd /d D:\Program Files\MySQL\MySQL Server 8.3\bin执行数据库备份命令 …

Java智慧校园系统源码 微信小程序+电子班牌

Java智慧校园系统源码 微信小程序电子班牌 通过设备管理对百纳智慧校园的智慧班牌以及百纳智慧屏&#xff08;校牌&#xff09;进行统一集中式管理&#xff0c;支持浏览所有设备的基本信息以及在离线状态&#xff0c;支持添加设备、设备一键开关机、一键重启、设置节假日开关机…