博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Redis 做分布式锁
阅读量:3947 次
发布时间:2019-05-24

本文共 4363 字,大约阅读时间需要 14 分钟。

Redis 做分布式锁

分布式锁也算是 Redis 中比较常见的使用场景。
问题场景:

例如一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读出用户的状态,然后在内存中进行修改、修改完成后,再存回去。在单线程中,这个操作没有任何问题,但是在多线程中,由于读取、修改、存这是三个操作,不是原子操作,所以在多线程中,这样会出现数据紊乱的问题。

对于这种问题,我们可以使用分布式锁来限制程序的并发执行。

1. 基本用法

分布式锁实现的思路很简单,就是进来一个线程先占位,当后面的线程进来操作时,发现已经有人占位了,就会放弃本次操作或者稍后再试。

在 Redis 中,占位一般使用 setnx 指令,先进来的线程先占位,线程的操作执行完成之后,在调用 del 指令释放位子。

根据上面的思路,构建代码如下:

public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis(); redis.execute(jedis->{
Long setnx = jedis.setnx("k1", "v1"); if (setnx == 1) {
// 无人占位 jedis.set("name","javaboy"); System.out.println(jedis.get("name")); // 释放资源 jedis.del("k1"); } else {
System.out.println("有人占位,停止/暂缓操作!"); } }); }}

上面的代码存在一个小问题,如果代码业务在执行的过程中抛异常或者挂掉了,这样会导致 del 指令没有被调用,这样,k1 无法释放,后面来的请求全部被堵塞在这里,锁也永远得不到释放。

要解决这个问题,我们可以给锁添加一个过期时间,确保锁在一定的时间之后,能够得到释放。改进后的代码如下:

public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis(); redis.execute(jedis->{
Long setnx = jedis.setnx("k1", "v1"); if (setnx == 1) {
// 给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放 jedis.expire("k1",5); // 无人占位 jedis.set("name","javaboy"); System.out.println(jedis.get("name")); // 释放资源 jedis.del("k1"); } else {
System.out.println("有人占位,停止/暂缓操作!"); } }); }}

这样改造之后,还有一个问题,就是在获取锁和设置过期时间之间如果服务器突然挂掉【比如突然断电】,这个时候锁被占用,无法及时得到释放,也会造成死锁,因为获取锁和设置过期时间是两个操作,不具备原子性

为了解决这个问题,从 Redis 2.8 开始,setnx 和 expire 可以通过一个命令来一起执行了,我们对上述代码再做改进:

public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis(); redis.execute(jedis -> {
// setnx 和 expire 合二为一使用 String set = jedis.set("k1", "v1", new SetParams().nx().ex(5)); if (null != set && "OK".equals(set)) {
// 无人占位 jedis.set("name", "javaboy"); System.out.println(jedis.get("name")); // 释放资源 jedis.del("k1"); } else {
System.out.println("有人占位,停止/暂缓操作!"); } }); }}

2. 解决超时问题

为了防止业务代码在执行的时候抛出异常,我们要给每一个锁添加一个超时时间,超时之后,锁会被自动释放,但是这也带来了一个新的问题:

如果要执行的业务非常耗时间,可能会出现紊乱。举个列子:第一个线程首先获取到锁,然后开始执行业务代码,但是业务代码比较耗时,执行了8秒,这样会在第一个线程的任务还未完成就会被释放掉,此时第二个线程开始获取到锁执行,在第二个线程刚刚执行了3秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,它释放的是第二个线程的锁,释放之后,第三个进程进来。

对于这两个问题,我们可以从两个角度入手:

  • 尽量避免在获取锁之后,执行耗时操作
  • 可以在锁上面做文章,将锁的 value 设置为一个随机字符串,每次释放锁的时候,都去比较随机字符串是否一致,如果一致,再去释放,否则不释放。

对于第二种解决方案,由于释放锁的时候,要去查看锁的 value,第二步比较 value 的值是否正确,第三步释放锁,有三个步骤,很明显三个步骤不具备原子性,为了解决这个问题,我们得引入 Lua 脚本。

Lua 脚本的优势:

  • 使用方便,Redis 中内置了对 Lua 脚本的支持
  • Lua 脚本可以在 Redis 服务端原子的执行多个 Redis 命令
  • 由于网络在很大程度上会影响到 Redis 性能,而使用 Lua 脚本可以让多个命令一次执行,可以有效解决网络给 Redis 带来的性能问题

在 Redis 中,使用 Lua 基本,大致上两种思路:

  • 提前在 Redis 服务端写好 Lua 脚本,然后在 Java 客户端去调用脚本(推荐)。
  • 可以直接在 Java 端写 Lua 脚本,写好之后,需要执行时,每次将脚本发送到 Redis 上去执行。

首先在 Redis 服务端创建 Lua 脚本,内容如下:

if redis.call("get",KEYS[1])==ARGV[1] then	return redis.call("del",KEYS[1])else 	return 0end

接下来,可以给 Lua 脚本求一个 SHA1 和,命令如下:

cat lua/redis_lock.lua | redis-cli -a javaboy script load --pipe

在这里插入图片描述

script load 这个命令会在 Redis 服务器缓存 Lua 脚本,并返回脚本内容的 SHA1 校验和,然后在 Java 端调用时,传入 SHA1 校验和作为参数,这样 Redis 服务端就知道执行哪个脚本了。

接下来,在 Java 客户端调用这个脚本。

public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis(); for (int i = 0; i < 2; i++) {
redis.execute(jedis -> {
// 1.先获取一个随机字符串 String value = UUID.randomUUID().toString(); // 2.获取锁 String set = jedis.set("k1", value, new SetParams().nx().ex(5)); // 3.判断是否成功拿到锁 if (null != set && "OK".equals(set)) {
// 4.具体的业务操作 jedis.set("name", "javaboy"); System.out.println(jedis.get("name")); // 5.释放锁 jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8" , Arrays.asList("k1"), Arrays.asList(value)); } else {
System.out.println("没拿到锁!"); } }); } }}

转载地址:http://apqwi.baihongyu.com/

你可能感兴趣的文章
一切悲剧都源于不当激励
查看>>
别把用户的高期望混同于好体验
查看>>
动机和机会:推动商业发展的引擎
查看>>
4个信号表明你是一个失败的领导
查看>>
成功谈判 你需要几个锦囊?
查看>>
一个人的宽度决定了他的高度
查看>>
善于拜访是另一种经营智慧
查看>>
打造新老员工双赢机制变对立为统一
查看>>
企业如何避免用错人
查看>>
打掉苹果“无与伦比”的傲慢(人民时评)
查看>>
Creating an Android Project
查看>>
Running Your App (android)
查看>>
Starting Another Activity
查看>>
Starting an Activity
查看>>
Stopping and Restarting an Activity
查看>>
Using the Support Library
查看>>
Creating a Fragment
查看>>
Building a Flexible UI
查看>>
Communicating with Other Fragments
查看>>
Saving Key-Value Sets
查看>>