我们启用了多线程去对 redis 中的 test_key 的值进行自增操作,理想情况,test_key 的值应该等于线程的数量,比如开了 10 个线程,test_key的值最终应该是 10。
方式一:加锁操作非原子性
在这个版本中,当线程 A get(key) 的值为空时,set key 的值为 1,并返回,这表示线程 A 获得了锁,可以继续执行后面的操作,否则需要一直循环去获取锁,直到 key 的值再次为空,重新获得锁,执行任务完毕后释放锁。
代码如下:
观察结果就发现,同时有多个线程输出的结果是一样的。乍一看上面加锁的代码逻辑似乎没啥问题,但是结果却事与愿违,原因是上面的代码 get(key) 和 set(key, value) 并不是原子性的,A 线程在 get(key) 的时候发现是空值,于是重新 set(key, value),但在 set 完成的前一刻,B 线程恰好 get(key) 的时候得到的还是空值,然后也顺利获得锁,导致数据被两个或多个线程同时修改,最后出现不一致,可以参考图2的过程。
方式二:使用 setnx 来实现
鉴于上面版本是由于命令不是原子性操作造成两个或多个线程同时获得锁的问题,这个版本改成使用 redis 的 setnx 命令来进行锁的查询和设置操作,setnx 即 set if not exists,顾名思义就是当key不存在的时候才设置 value,并返回 1,如果 key 已经存在,则不进行任何操作,返回 0。
代码改进如下:
结果是正确的,但是如果满足于此,还是会出问题的,比如假设 A 线程获得了锁后,由于某种异常原因导致线程 crash了,一直不释放锁呢?我们稍微改一下测试用例的 increase 函数,模拟某个线程在释放锁之前因为异常退出。
方式一:加锁操作非原子性
在这个版本中,当线程 A get(key) 的值为空时,set key 的值为 1,并返回,这表示线程 A 获得了锁,可以继续执行后面的操作,否则需要一直循环去获取锁,直到 key 的值再次为空,重新获得锁,执行任务完毕后释放锁。
代码如下:
观察结果就发现,同时有多个线程输出的结果是一样的。乍一看上面加锁的代码逻辑似乎没啥问题,但是结果却事与愿违,原因是上面的代码 get(key) 和 set(key, value) 并不是原子性的,A 线程在 get(key) 的时候发现是空值,于是重新 set(key, value),但在 set 完成的前一刻,B 线程恰好 get(key) 的时候得到的还是空值,然后也顺利获得锁,导致数据被两个或多个线程同时修改,最后出现不一致,可以参考图2的过程。
方式二:使用 setnx 来实现
鉴于上面版本是由于命令不是原子性操作造成两个或多个线程同时获得锁的问题,这个版本改成使用 redis 的 setnx 命令来进行锁的查询和设置操作,setnx 即 set if not exists,顾名思义就是当key不存在的时候才设置 value,并返回 1,如果 key 已经存在,则不进行任何操作,返回 0。
代码改进如下:
结果是正确的,但是如果满足于此,还是会出问题的,比如假设 A 线程获得了锁后,由于某种异常原因导致线程 crash了,一直不释放锁呢?我们稍微改一下测试用例的 increase 函数,模拟某个线程在释放锁之前因为异常退出。