并发控制的几种方案
结合之前的文件系统的案例, 假设有多个线程过来,要并发的给/workspace/projects/helloworld下的README.txt修改文件名,这个时候就要进行并发的控制,避免多线程的并发安全问题.
基于version的乐观锁,先获取要修改的信息的版本号,然后和自己的版本号对比,如果版本号不一样了,就意味着有别的线程已经修改过这个数据了,必须重新获取新的版本号再次尝试修改
然后是悲观锁,就是上来就先尝试给这个数据加锁,如果要是加锁成功了,这个时候这个数据只有当前这个线程可以操作,其他线程过来也尝试加锁,发现已经被上一个线程加锁了,就一直重新上锁,直到获得锁以后才可以对数据进行操作
全局锁是粒度最粗的
全局锁的实现
就是直接锁掉整个要操作的index
首先,第一步,上锁1
2PUT /fs/lock/global/_create
{}
- fs: 要上锁的index
- lock:就是指定一个对这个index上全局锁的一个type
- global: 就是这个全局锁对应的这个doc的id
- _create: 强制必须是创建,如果这个doc已经存在,就创建失败,报错
其实就是添加了一个空的document,语法是和添加document一样的 /index/type/id
创建完成后,如果另一个线程同时尝试上锁,也就是再执行一次这个请求1
2PUT /fs/lock/global/_create
{}
返回值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[lock][global]: version conflict, document already exists (current version [1])",
"index_uuid": "RWijfql1Qlqa_OPoor0Ubw",
"shard": "2",
"index": "fs"
}
],
"type": "version_conflict_engine_exception",
"reason": "[lock][global]: version conflict, document already exists (current version [1])",
"index_uuid": "RWijfql1Qlqa_OPoor0Ubw",
"shard": "2",
"index": "fs"
},
"status": 409
}
可以看到,锁已经存在的情况下,其他线程再尝试加锁就会报错,没获得锁的线程就一直重复尝试上锁,直到成功
此时,第一个成功上锁的线程就可以对document进行各种操作,比如更新文件名1
2
3
4
5
6POST /fs/file/1/_update
{
"doc": {
"name": "README1.txt"
}
}
返回值:1
2
3
4
5
6
7
8
9
10
11
12{
"_index": "fs",
"_type": "file",
"_id": "1",
"_version": 2,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
}
}
修改完成,最后一步,释放锁,也就是删除/fs/lock/global这个document
1 | DELETE /fs/lock/global |
第二个线程之前上锁失败,但是这时候第一个线程已经释放了锁,所以第二个线程上锁成功,然后他也可以对这个index里面的数据做各种操作,最后一步delete掉,释放锁就好了
全局锁的优缺点
优点: 操作简单,使用容易,成本低
缺点: 直接把整个index锁上了,这时候别的线程对index中所有的doc的操作都会被block住,导致整个系统的并发能力很低
适用场景
上锁释放锁的操作不是很频繁,每次上锁之后,执行的操作耗时不会太长,可以使用这种方式, 方便