分布式锁——redis实现

使用Redis实现了一个锁,命名为:RedisLock。

一、原理介绍

1.1 RedisLock特性

用最简单的方式实现一个redis分布式锁,该锁具有如下特性:

  • 阻塞式:在加锁时,如果获取不到,则进入自旋状态;
  • 非阻塞式:在加锁时,如果获取不到,立刻返回加锁失败;
  • 可重入:同一个线程多次请求加锁,对资源做+1操作,解锁时,资源必须等于0才算完全解除锁定;
  • 锁超时:加锁时,可以设置超时时间,如果在这个时间内没有获取到锁,则返回加锁失败;
  • 锁过期:不允许用户设置过期时间,内部设置了一个默认60s的锁过期时间,防止锁长时间无法释放。

1.2 RedisLock对外提供的方法

  • void lock():阻塞式锁,尝试获取锁、直到成功获取
  • boolean tryLock():非阻塞式锁,只尝试一次,不论成功失败都返回
  • boolean tryLock(int timeout):阻塞式锁,在超时时间内尝试获取

1.3 分布式锁使用注意事项

注意提高下分布式锁的优先级,当业务完成之后再释放。

举个例子,我有个业务,需要先查询数据库,如果没有该记录则创建一条,所以是两条sql:查询和插入,我为了保证分布式情况下只允许插入一条,就同时使用了事务和分布式锁。

我司中间件团队提供了基于注解的分布式锁,我在使用分布式注解时,同时给方法加上事务注解和分布式锁注解,结果偶然情况下会插入两条记录,排查分布式锁半天,发现锁没问题,在zk中创建了临时顺序节点,后来觉察出来可能是事务实行完还没提交,新的请求又进来了,所以把分布式锁提到外面去了。

分布式锁没起作用的代码:

public class XxxFacadeImpl{
    public void methodA() {
		    methodB();
		}
}

public class XxxServiceImpl {
	@KLock(zk)
	@Transactional
	public void methodB() {
	    select(id);
	    if (没查到) {
	        insert(xxxPO);
	    }
	}
}

修改后:

public class XxxFacadeImpl{
		@KLock(zk)
    public void methodA() {
		    methodB();
		}
}

public class XxxServiceImpl {
	@Transactional
	public void methodB() {
	    select(id);
	    if (没查到) {
	        insert(xxxPO);
	    }
	}
}

 

二、Redis实现分布式锁

package utils.lock;

import org.apache.commons.lang3.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.UUID;

public class RedisLock {
    /**
     * 每个线程持有自己的uuid,用于区分不同的锁
     */
    private static final ThreadLocal<String> identifier = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());

    private static final String LOCK_MSG = "OK";
    private static final Long UNLOCK_MSG = 1L;

    /**
     * 锁过期时间默认值
     */
    private static final int DEFAULT_EXPIRE_TIME = 60;

    /**
     * 阻塞式获取锁的间隔时间
     */
    private static final long DEFAULT_SLEEP_TIME = 100;

    // Redis连接池
    private static String REDIS_IP = "192.168.160.128";
    private static int REDIS_PORT = 6379;
    private static JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), REDIS_IP, REDIS_PORT);

    // 加锁的键
    private String lockKey;

    // 重入锁计数器
    private int counter = 0;

    public RedisLock(String lockKey) {
        this.lockKey = lockKey;
    }


    /*******************************************
     * 锁对外提供的使用方法
     *******************************************/

    /**
     * 阻塞式锁,尝试获取锁、直到成功
     * @throws InterruptedException
     */
    public void lock() throws InterruptedException {
        lock(lockKey, identifier.get());
    }

    /**
     * 非阻塞式锁,只尝试一次,无论是否获取到锁都返回
     * @return
     * @throws InterruptedException
     */
    public boolean tryLock() throws InterruptedException {
        return tryLock(lockKey, identifier.get());
    }

    /**
     * 阻塞式锁,在超时时间内尝试获取锁,默认100ms尝试一次
     * @param timeout
     * @return
     * @throws InterruptedException
     */
    public boolean tryLock(int timeout) throws InterruptedException {
        return tryLock(lockKey, identifier.get(), timeout);
    }

    public boolean unlock() {
        return unlock(lockKey, identifier.get());
    }

    public boolean isLocked() {
        Jedis jedis = jedisPool.getResource();
        String redisVal = jedis.get(lockKey);
        return redisVal != null && redisVal.equals(identifier.get());
    }

    public void flushAll() {
        Jedis jedis = jedisPool.getResource();
        jedis.flushAll();
    }

    public void shutdown() {
        jedisPool.destroy();
    }


    /*******************************************
     * 以下是锁的内部实现
     *******************************************/
    private void lock(String key, String value) throws InterruptedException {
        Jedis jedis = jedisPool.getResource();
        while (true) {
            if (setLockToRedis(key, value, jedis)) {
                return;
            }
        }
    }

    private boolean tryLock(String key, String value, int timeout) throws InterruptedException {
        Jedis jedis = jedisPool.getResource();

        while (timeout >= 0) {
            if (setLockToRedis(key, value, jedis)) {
                return true;
            }
            timeout -= DEFAULT_SLEEP_TIME;
        }

        return false;
    }

    private boolean tryLock(String key, String value) throws InterruptedException {
        Jedis jedis = jedisPool.getResource();
        return setLockToRedis(key, value, jedis);
    }

    private boolean setLockToRedis(String key, String value, Jedis jedis) throws InterruptedException {
        SetParams params = new SetParams();
        params.nx().ex(DEFAULT_EXPIRE_TIME); // setNx,带超时时间

        String result = jedis.set(key, value, params);
        if (LOCK_MSG.equals(result)) {
            jedis.close();
            return true;
        }

        // 重入
        String redisVal = jedis.get(key);
        if (StringUtils.isNotBlank(redisVal) && redisVal.equals(value)) {
            counter++;
            jedis.close();
            return true;
        }

        Thread.sleep(DEFAULT_SLEEP_TIME);
        return false;
    }

    private boolean unlock(String key, String value) {
        if (counter > 0) {
            counter--;
            return true;
        }


        Jedis jedis = jedisPool.getResource();

        // 只有缓存的value和方法入参value相等时才删除缓存,保证只有加锁的线程有权限解锁
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));

        jedis.close();
        return UNLOCK_MSG.equals(result);
    }

}

 

三、测试

本地启动3个线程模拟分布式环境(条件不允许,凑合测吧)

3.1 阻塞式锁

package mytest.redis;

import utils.lock.RedisLock;

import java.util.concurrent.*;


/**
 * @Description 分布式锁的测试
 * @Author lilong
 * @Date 2019-03-27 20:38
 */
public class RedisLockTest {
    public static void main(String[] args) {
        RedisLock redisLock = new RedisLock("testLock");

        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        CountDownLatch countDownLatch = new CountDownLatch(3);

        for (int i = 0; i < 1; i++) {
            threadPool.execute(() -> {
                try {
                    testBlockedLock(redisLock);
                    System.out.println("######### " + Thread.currentThread().getId() + " 馍馍片");
                    countDownLatch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Game Over!");
        threadPool.shutdown();
        redisLock.flushAll(); // 清空缓存
    }

    /**
     * 阻塞式锁
     *
     * @param redisLock
     * @throws InterruptedException
     */
    private static void testBlockedLock(RedisLock redisLock) throws InterruptedException {
        System.out.println("######### " + Thread.currentThread().getId() + " 加锁中...");
        redisLock.lock();
        boolean isLocked = redisLock.isLocked();

        try {
            System.out.println("######### " + Thread.currentThread().getId() + (isLocked ? " 加锁成功!" : " 加锁失败!"));
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (redisLock.unlock()) {
                System.out.println("######### " + Thread.currentThread().getId() + " 解锁成功");
            } else {
                System.out.println("######### " + Thread.currentThread().getId() + " 解锁失败");
            }
        }
    }
}

打印:

符合happens-before语义中的:解锁先于加锁。

3.2 非阻塞式锁

复用上面的main方法,更改测试锁的方法:
 

/**
 * 非阻塞式锁
 *
 * @param redisLock
 * @throws InterruptedException
 */
private static void testNonBlockingLock(RedisLock redisLock) throws InterruptedException {
    System.out.println("######### " + Thread.currentThread().getId() + " 加锁中...");
    boolean isLocked = redisLock.tryLock();

    try {
        System.out.println("######### " + Thread.currentThread().getId() + (isLocked ? " 加锁成功!" : " 加锁失败!"));
        Thread.sleep(3000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (redisLock.unlock()) {
            System.out.println("######### " + Thread.currentThread().getId() + " 解锁成功");
        } else {
            System.out.println("######### " + Thread.currentThread().getId() + " 解锁失败");
        }
    }
}

打印:

只有线程16加锁成功,其他线程都加锁失败。

3.3 超时锁

timeout设置为1s,而线程睡眠3s,因此只有一个线程能拿到锁,其它线程都会在等待获取锁的过程中超时。

/**
 * 带超时时间的阻塞式锁
 *
 * @param redisLock
 * @throws InterruptedException
 */
private static void testBlockedLockWithTimeout(RedisLock redisLock) throws InterruptedException {
    System.out.println("######### " + Thread.currentThread().getId() + " 加锁中...");
    boolean isLocked = redisLock.tryLock(1000);

    try {
        System.out.println("######### " + Thread.currentThread().getId() + (isLocked ? " 加锁成功!" : " 加锁失败!"));
        Thread.sleep(3000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (redisLock.unlock()) {
            System.out.println("######### " + Thread.currentThread().getId() + " 解锁成功");
        } else {
            System.out.println("######### " + Thread.currentThread().getId() + " 解锁失败");
        }
    }
}

打印:

只有线程15加锁成功,其他线程加锁失败。

3.4 重入锁

/**
 * 重入锁
 *
 * @param redisLock
 * @throws InterruptedException
 */
private static void testReentrantLock(RedisLock redisLock) throws InterruptedException {
    int i = 1;
    try {
        System.out.println("######### " + Thread.currentThread().getId() + " 第" + i +  "次加锁中...");
        redisLock.lock();
        boolean isLocked = redisLock.isLocked();
        System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" +(isLocked ? "加锁成功!" : "加锁失败!"));
        Thread.sleep(1000);

        i++;
        System.out.println("######### " + Thread.currentThread().getId() + " 第" + i +  "次加锁中...");
        redisLock.lock();
        isLocked = redisLock.isLocked();
        System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" +(isLocked ? "加锁成功!" : "加锁失败!"));
        Thread.sleep(1000);

        i++;
        System.out.println("######### " + Thread.currentThread().getId() + " 第" + i +  "次加锁中...");
        redisLock.lock();
        isLocked = redisLock.isLocked();
        System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" +(isLocked ? "加锁成功!" : "加锁失败!"));
        Thread.sleep(1000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        i = 1;
        if (redisLock.unlock()) {
            System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁成功");
        } else {
            System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁失败");
        }

        i++;
        if (redisLock.unlock()) {
            System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁成功");
        } else {
            System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁失败");
        }

        i++;
        if (redisLock.unlock()) {
            System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁成功");
        } else {
            System.out.println("######### " + Thread.currentThread().getId() + " 第" + i + "次" + " 解锁失败");
        }
    }
}

打印:

可以看到,对于同一把锁,多个线程可以多次获取到锁(可重入),当然了,加多少次锁就要释放多少次锁,这样才能完全释放掉锁,别的线程才能继续获取。

4 其他

4.1 Redis安装

1)去redis官网(https://redis.io/)下载最新版本,我用的redis-5.0.4.tar.gz,下载完拷贝到虚拟机里边

2)解压文件:tar xzvfp redis-5.0.4.tar.gz,会生成一个文件夹:redis-5.0.4

3)cd redis-5.0.4  ,进入文件夹,输入make命令进行编译,如果编译不通过, 可能会报缺少编译环境c、cpp啥的,需要先解决掉这些问题;

4)编译完成之后,进入 src文件夹,运行./redis-server,启动redis服务端;

5)如果客户端提示权限失败啥的,可能还要在运行时加上配置文件:

[root@localhost redis-5.0.4]# ./src/redis-server redis.conf

 

4.2 查看Redis中的保存的锁

1)运行redis客户端

[root@localhost src]# ./redis-cli

2)查看锁记录

我们刚才的案例中保存的锁的名称叫“testLock”,我们可以用“get testLock”命令查看保存的内容。

为了看到加锁、解锁过程中redis记录的变化,我们启用单个线程,并且在加锁后、解锁后打断点:

  • 未加锁:
  • 加锁时:
  • 解锁后:

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 在Spring Boot中集成Redis分布式锁可以使用Redisson来实现Redisson是一个基于Redis的分布式对象和服务框架,内部已经实现Redis分布式锁,使用起来更加方便和稳定。通过Redisson,可以使用RedLock算法来实现分布式锁,确保锁的正确性和可靠性。在Redis集群环境下,可能存在锁失效或死锁的问题,但使用Redisson的分布式锁可以解决这个问题。此外,分布式锁是为了解决分布式系统中控制共享资源访问的问题,因为在分布式系统中,多线程、多进程分布在不同机器上,传统的并发控制锁策略失效。因此,通过集成Redis分布式锁,可以有效地控制共享资源的访问。 #### 引用[.reference_title] - *1* *2* [springboot集成redis 分布式锁(redistemplate,lua,redisson)](https://blog.csdn.net/jun2571/article/details/130382023)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Springboot集成Redis——实现分布式锁](https://blog.csdn.net/tang_seven/article/details/126769580)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值