Redis(四)实现乐观锁
2022-09-22 22:46:44

目录

[TOC]


首先介绍为什么加锁,什么悲观锁、乐观锁,然后再说明redis为什么能实现乐观锁?

为什么要加锁?

叨咕一句

在使用一些工具、功能之前,都要明白他有什么作用和意义。
不能为了学习而学习,为了使用而使用,也不能会了一样工具就到处使用。
合适才是最好的,应该是为了解决某些问题而引入使用。这也是一种架构思想

锁存在的意义是什么呢

模拟一种场景:

小明:代表——>A线程或者说A客户端

小强:代表——>B线程或者说B客户端

他们是好朋友,共享一个银行账户,想去坐火车去存钱,目前只有一张火车票了:代表——>共享资源

在不加锁的情况下

小明和小强同时查询余票,他们都查询到了一张余票,然后几乎同时购买,结构全购买成功了,所以他们挤着同一个座位…(错误一

然后去银行存款,他们分别在两家银行存钱,都查询到账户余额是500,于是小明存了500(余额变成1000),小强在另一家银行存了100,银行操作成功结果 原来的1000被600覆盖掉了(错误二

并发环境下典型的冲突

  • 脏读(错误一):当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是1,用户B把值改为0,用户A读到的值仍为1。

  • 丢失更新(错误二):一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从500改为1000,用户B把值从500改为600,则用户A丢失了他的更新。

对于这种现象,就可以通过加锁来解决,比如使用(java中的synchronized,悲观锁)

  1. A、B读取的值都是1,那么在A修改值为0的时间范围内,先加锁,B此时是不能修改这个值的。
  2. 在A修改完值等于0后,会释放锁
  3. 然后B才能修改值,B再加锁,并修改值等于-1,然后释放锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

Redis的Watch命令

watch有什么作用?

Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行(EXEC)之前这个(或这些) key 被其他命令所改动,那么事务将被打断

基于watch命令的机制,就可以实现乐观锁的功能

语法:

1
WATCH key [key ...]

实例

  1. 客户端A,首先执行:
1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> set money 100  # 设置money 为100
OK
127.0.0.1:6379> watch money # 监视 money 对象
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> incrby money 20 # 增加 money 20 ,存入队列
QUEUED
127.0.0.1:6379> get money # 获取 money ,存入队列
QUEUED
# 先不执行事务
  1. 客户端B:
1
2
3
4
5
# 客户端B 在 客户端A还没有执行事务前,设置money值为500
127.0.0.1:6379> set money 500
OK
127.0.0.1:6379> get money # 查看 money
"500"
  1. 客户端A,再执行:
1
2
3
# 然后 客户端A 再执行职务,
127.0.0.1:6379> exec
(nil) # 执行失败

以上面代码为例:如果事务正常执行了,想要下次的事务还监视这个key的话,需要再写一遍watch money,然后再开启事务multi,也就是要成对出现

UNWATCH

取消 WATCH 命令对所有 key 的监视。

如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。


参考文章: