遇到的问题:
无论是在文章中还是在视频中,作者总是不断的在说事务会对性能有很大的影响,而具体有什么样的影响呢? 如果有MVCC影响还会很大吗? 带着这样的疑问,我对学到的东西进一步进行总结
事务性能影响的关键点在于"锁" 如果我们在事务中需要对记录做一些修改操作那么将会把记录行进行锁定,如果我们是一个范围这里还有可能有间隙锁,这个表也会加上意向锁. 而这些锁的释放是在等事务提交或是回滚时才会释放(上锁是在执行到这条更新语句时,而锁的释放是在整个事务提交),我们知道如果我们对表加了意向锁对行加了排它锁另外的事务需要执行时就需要等待锁的资源释放,这就是影响性能的罪魁祸首了.
我们在进一步看如果有了MVCC会好一些吗?
其实MVCC对性能是有提升的,但只是读操作因为有了版本控制我们可以读取到不同版本的记录(当然这个和隔离级别有关系)这样减少了读锁的竞争,但是对于写还是需要加排它锁
扩展1: 乐观锁
如果我们要减少事务的使用有别的方法来减少数据冲突吗? 有的,我们可以通过使用乐观锁的方式来实现逻辑判断版本冲突 通常的做法是 在记录中添加一个字段表示版本号,在获取的时候获取版本号,在更新时加上版本号的判断 如下面的sql
select * from user where id = 1;
// 加入查出来的的字段有: name=zhangsan age=18 version=1;
update user set age =19 ,version=2 where id=1 and version=1;
我们在更新的是添加了版本号的判断如果在select到update的这段时间有另外的事务对这行数据做了修改,那么在更新时候就会失败,我们可以选择回滚或是重试.
扩展2: 分布式锁
分布式锁也是解决资源竞争的一种方式,算是悲观锁的一种方式(只要提前加锁的都是悲观锁), 正常情况下我们通过加锁的方式可以实现单进程的资源管理例如下面的golang代码
阶段1: 单进程通过程序加锁的方式实现资源管理
// Increment safely increases the counter by 1
func (r *Resource) Increment() {
r.mu.Lock() // 加锁 r.counter++ // 修改共享资源
fmt.Println("Counter:", r.counter)
r.mu.Unlock() // 解锁
}
当业务快速发展需要我们横向扩展业务,于是我们的业务就变成了下面的样子:
这时候我们的程序加锁就没用了(一起执行的有多个进程,程序锁只能保证单进程只有一个线程或是协程在执行,但多个进程就不能保证只有一个在执行了) 这时候就需要我们的分布式锁了.
一般用Redis做分布式锁的比较多,下面我们说下执行的步骤:
进程在执行业务前先去redis获得锁
setnx 如果获得锁成功了就继续执行业务,如果失败了就自旋等待锁被释放
但这个会有一些问题:
- 无限等待: 如果获得锁的进程挂掉了,谁来释放呢?
- 解决办法: 在setnx时候添加超时时间
- 如果在超时时间内未完成业务,锁会主动被释放,另外的进程会获得锁并执行业务,这样保护就失效了
- 解决办法: 延长锁的超时时间
- 启动检查进程,进程是否还存在,如果存在重置超时时间(续租)
- 当出现超时时间内未完成业务(假设为进程A),超时自动释放锁后,另外的进程(假设为进程B)获得了锁但此时进程A又完成了业务,这时候就会释放锁,但是这个锁已经不是进程A的那一把锁了,而是进程B的锁,所以就出现了进程A释放了
进程B的锁
- 将锁的名称加上进程/线程ID 标记这个锁和进程/线程的关系
其实上面说的这些原理,一个Java的客户端库
Redisson已经实现了
- 将锁的名称加上进程/线程ID 标记这个锁和进程/线程的关系
其实上面说的这些原理,一个Java的客户端库
在lang中也有相应的库 redsync 下面是使用redsync的一个例子(AI生成的未进行验证)
大概思路就是尝试去获取锁,如果未获得就自旋等待锁被释放,而获得锁的就进行业务处理
package main
import (
"context"
"fmt"
"time"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
"github.com/redis/go-redis/v9"
)
func acquireLockWithRetry(mutex *redsync.Mutex, retryInterval, retryTimeout time.Duration) error {
// 计算重试截止时间
deadline := time.Now().Add(retryTimeout)
for {
// 尝试获取锁
if err := mutex.Lock(); err == nil {
return nil
}
// 检查是否超过了重试的截止时间
if time.Now().After(deadline) {
return fmt.Errorf("failed to acquire lock within the given timeframe")
}
// 等待一段时间后重试
time.Sleep(retryInterval)
}
}
func main() {
// 创建 Redis 客户端
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 服务器地址
})
// 使用 goredis 将 go-redis 适配为 redsync 的 Pool 接口
pool := goredis.NewPool(client)
// 创建 redsync 实例
rs := redsync.New(pool)
// 创建一个新的互斥锁实例
mutex := rs.NewMutex("my-distributed-lock")
// 尝试获取锁并重试
retryInterval := 200 * time.Millisecond // 重试间隔
retryTimeout := 5 * time.Second // 总重试时间
if err := acquireLockWithRetry(mutex, retryInterval, retryTimeout); err != nil {
fmt.Println("无法获取锁:", err)
return
}
fmt.Println("锁已获得")
// 模拟需要锁保护的任务
time.Sleep(2 * time.Second)
// 释放锁
if ok, err := mutex.Unlock(); !ok || err != nil {
fmt.Println("无法释放锁:", err)
} else {
fmt.Println("锁已释放")
}
}
扩展3: MVCC
MVCC(多版本并发控制): 通过维护数据的多个版本来允许并发事务读取和写入数据库,而不会相互阻塞. 关键特征:
- 非阻塞读: 在 MVCC 系统中,读操作不会因为写操作而被阻塞。所有的读取都访问数据的快照(即一个时间点上的一致性视图)。这大大提高了并发读的性能,因为读操作无需等待写操作完成
- 写隔离: 写操作对数据做出的更改不会立即对其他并发事务可见。这些更改仅在事务提交后对其他事务可见
- 多个版本: 数据库维护每条数据多个版本的快照。每当数据被更新时,旧版本不会立即被覆盖,而是保存为“旧快照”