Redis实现分布式锁

Redis分布式锁逻辑实现

分布式系统要访问共享资源,为了避免并发访问资源带来错误,我们为共享资源添加一把锁,让各个访问互斥,保证并发访问的安全性。

例如有这么一个场景:当前项目是一个处理贷款审批流程的项目,例如订单A当前在B流程池中,现在用户C对订单A进行引入操作(即将订单A接收至自己名下)。与此同时,用户D也可能对订单A进行同样的操作,这样显然会造成问题。这时候我们需要用到分布式锁来解决这类问题,保证在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
伪代码如下:

// 加锁
if(set(lock_apply_A,1) == 1){
    // 添加锁超时时间
    expire(lock_apply_A,30)
    try {
        do something ......
    } finally {
        // 释放锁
        del(lock_sale_商品ID)
    }
}

优化点1,例如线程P1加锁成功之后挂掉了,那就无法进行后续的添加锁超时时间等操作,造成死锁。所以我们在加锁的同时能设置好超时时间就最好不过了。优化伪代码:

if(set(lock_apply_A,1,30,NX) == 1){
    try {
        do something ......
    } finally {
        del(lock_apply_A)
    }
}

优化点2,例如线程P1获取锁成功之后,30秒之内依然没有执行完,这时候锁过期会自动释放。然后此时线程P2成功获取到了锁,执行过程中。P1的do something执行完毕,进行删除锁动作,这时候P2还没执行完,P1删除的锁实际上是P2的锁。为了避免这种情况,在删除之前需要验证当前线程删除的是自己的锁。优化伪代码:

String threadId = Thread.currentThread().getId()
if(set(lock_apply_A,threadId ,30,NX) == 1){
    try {
        do something ......
    } finally {
        if(threadId .equals(get(key))){
             del(lock_apply_A)
        }
    }
}

spring-boot下的redis分布式锁实现

新建redis分布式锁实现类
package com.example.demo.service.redislocker;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @author liwenhuan
 * @date 2020/5/27 10:58
 */
@Slf4j
@Service
public class RedisLockerService {
    /**
     * redis的key值前缀
     */
    private static String KEY_PRE = "SPRINGBOOT:STRING:REDISLOCKER:";

    /**
     * 默认持有时间10分钟
     */
    private static long DEFAULT_KEEP_TIME = 600000L;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    @Qualifier("lockScript")
    private RedisScript<Boolean> lockScript;

    @Autowired
    @Qualifier("unlockScript")
    private RedisScript<String> unlockScript;

    /**
     * 加锁
     * 持有时间默认为10分钟
     *
     * @param key key值
     * @return 加锁结果:false 失败,true 成功
     */
    public boolean tryLock(String key) {
        return this.tryLock(key, DEFAULT_KEEP_TIME);
    }

    /**
     * 加锁
     *
     * @param key      key值
     * @param keepTime 保持时间
     * @return 加锁结果:false 失败,true 成功
     */
    public boolean tryLock(String key, long keepTime) {
        log.info("【加锁】开始,key={}, keepTime={}", key, keepTime);
        String redisKey = KEY_PRE + key;
        String threadId = String.valueOf(Thread.currentThread().getId());
        List<String> keys = new ArrayList<>();
        keys.add(redisKey);
        Boolean result = stringRedisTemplate.execute(lockScript, keys, threadId, String.valueOf(keepTime));
        log.info("【加锁】结束,key={}, keepTime={}, result={}", key, keepTime, result);
        return result == null ? false : result;
    }

    /**
     * 解锁
     *
     * @param key key值
     */
    public void unlock(String key) {
        log.info("【解锁】开始,key={}", key);
        String redisKey = KEY_PRE + key;
        String threadId = String.valueOf(Thread.currentThread().getId());
        List<String> keys = new ArrayList<>();
        keys.add(redisKey);
        String result = stringRedisTemplate.execute(unlockScript, keys, threadId);
        if ("0".equals(result)) {
            log.warn("【解锁】失败,锁不存在,key={}", key);
        } else if ("2".equals(result)) {
            log.warn("【解锁】失败,锁不属于当前线程,key={}", key);
        } else {
            log.info("【解锁】结束,key={}", key);
        }
    }
}
Lua脚本编写类
package com.example.demo.service.redislocker;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

/**
 * lua脚本
 *
 * @author liwenhuan
 * @date 2020-05-27
 */
@Configuration
public class RedisScriptConfig {

    /**
     * 加锁脚本:加锁成功则返回1,失败则返回0
     */
    @Bean(name = "lockScript")
    public RedisScript<Boolean> lockScript() {
        StringBuilder lockScript = new StringBuilder();
        // 加锁,并设置对应value为当前线程id
        lockScript.append("if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then ");
        // 加锁成功后,为锁设置超时时间
        lockScript.append("  redis.call('pexpire', KEYS[1], ARGV[2]); ");
        lockScript.append("  return 1; ");
        lockScript.append("else ");
        lockScript.append("  return 0; ");
        lockScript.append("end; ");

        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(lockScript.toString());
        redisScript.setResultType(Boolean.class);

        return redisScript;
    }

    /**
     * 解锁脚本:1-解锁成功;2-锁不属于当前线程;0-未找到锁
     */
    @Bean(name = "unlockScript")
    public RedisScript<String> unlockScript() {
        StringBuilder unlockScript = new StringBuilder();
        // 检查锁是否存在
        unlockScript.append("if (redis.call('exists', KEYS[1]) == 1) then ");
        // 存在则获取锁对应的value,value值为当前线程id才进行删除锁操作
        unlockScript.append("  if (redis.call('get', KEYS[1]) == ARGV[1]) then ");
        unlockScript.append("    redis.call('del', KEYS[1]); ");
        unlockScript.append("    return '1'; ");
        unlockScript.append("  else ");
        unlockScript.append("    return '2'; ");
        unlockScript.append("  end; ");
        unlockScript.append("else ");
        unlockScript.append("  return '0'; ");
        unlockScript.append("end; ");

        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(unlockScript.toString());
        redisScript.setResultType(String.class);

        return redisScript;
    }
}
测试
  public static void main(String args[]) {
      RedisLockerService service = new RedisLockerService();
        // 加锁结果默认false
        boolean lockSuccess = false;
        String key = "abc";
        try {
            lockSuccess = service.tryLock(key);
            if (lockSuccess) {
                // 加锁成功则进行相关业务逻辑处理
            } else {
                // 加锁失败,提示当前操作不允许(结合实际业务)
            }
        } finally {
            // 加锁成功
            if (lockSuccess) {
                // 才进行解锁
                service.unlock(key);
            }
        }
    }
PS:使用Lua脚本的好处

1.减少网络开销。可以将多个请求通过脚本的形式一次性发送,减少网络时延。
2.原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入,无需使用事务。
3.复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。