一、分布式锁

分布式锁,是一种思想,它的实现方式有很多。比如,我们将沙滩当做分布式锁的组件,那么它看起来应该是这样的:

加锁

在沙滩上踩一脚,留下自己的脚印,就对应了加锁操作。其他进程或者线程,看到沙滩上已经有脚印,证明锁已被别人持有,则等待。

解锁

把脚印从沙滩上抹去,就是解锁的过程。

锁超时

为了避免死锁,我们可以设置一阵风,在单位时间后刮起,将脚印自动抹去。

分布式锁的实现有很多,比如基于数据库、memcached、Redis、系统文件、zookeeper等。它们的核心的理念跟上面的过程大致相同。

二、redis

我们先来看如何通过单节点Redis实现一个简单的分布式锁。

1、加锁
加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。

值得注意的是:
lockValue 是客户端生成的唯一的字符串。
flag 代表只在键不存在时,才对键进行设置操作。
timeout 设置键的过期时间。

这样,如果上面的命令执行成功,则证明客户端获取到了锁。
LUA代码如下

----
--锁的名称
local lockName=KEYS[1]
--锁的value
local lockValue=ARGV[1]
--过期时间
local timeout=tonumber(ARGV[2])
--尝试进行加锁
local flag=redis.call('setnx',lockName,lockValue)
--判断是否获得锁
if flag==1 then
   redis.call('expire',lockName,timeout)
end

--返回标识
return flag

2、解锁
解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。

为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
LUA代码如下

--锁的名称
local lockName=KEYS[1]
--锁的value
local lockValue=ARGV[1]
--判断锁是否存在,以及锁的内容是否为自己加的
local value=redis.call('get', lockName)
--判断是否相同
if value == lockValue then
    redis.call('del', lockName)
    return 1
end
return 0

三、Java使用Redis的lua脚本,实现分布式锁,及封装

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application.yml

spring:
  redis:
    host: xxx.xxx.xxx.xx
    port: 6379

LockUtil.java

@Component
public class LockUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    //加锁的lua脚本
    private String lockLua = "--锁的名称\n" +
            "local lockName=KEYS[1]\n" +
            "--锁的value\n" +
            "local lockValue=ARGV[1]\n" +
            "--过期时间 秒\n" +
            "local timeout=tonumber(ARGV[2])\n" +
            "--尝试进行加锁\n" +
            "local flag=redis.call('setnx', lockName, lockValue)\n" +
            "--判断是否获得锁\n" +
            "if flag==1 then\n" +
            "--获得分布式锁,设置过期时间\n" +
            "redis.call('expire', lockName, timeout)\n" +
            "end\n" +
            "--返回标识\n" +
            "return flag ";

    //解锁的lua脚本
    private String unLockLua = "--锁的名称\n" +
            "local lockName=KEYS[1]\n" +
            "--锁的value\n" +
            "local lockValue=ARGV[1]\n" +
            "--判断锁是否存在,以及锁的内容是否为自己加的\n" +
            "local value=redis.call('get', lockName)\n" +
            "--判断是否相同\n" +
            "if value == lockValue then\n" +
            "     redis.call('del', lockName)\n" +
            "     return 1\n" +
            "end\n" +
            "return 0";

    private ThreadLocal<String> tokens = new ThreadLocal<>();

    /**
     * 加锁(默认超时时间30s)
     * @return
     */
    public void lock(String lockName){
        lock(lockName, 30);
    }
	/**
     * 加锁可自定义超时时间timeout
     * @return
     */
    public void lock(String lockName, Integer timeout){

        String token = UUID.randomUUID().toString();
        //设置给threadLocal
        tokens.set(token);

        //分布式锁 - 加锁
        Long flag = (Long) redisTemplate.execute(new DefaultRedisScript(lockLua, Long.class),
                Collections.singletonList(lockName),
                token, timeout + ""
        );

        System.out.println("获得锁的结果:" + flag);

        //设置锁的自旋
        if (flag == 0) {
            //未获得锁
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock(lockName, timeout);
        }
    }

    /**
     * 解锁
     * @return
     */
    public boolean unlock(String lockName){

        //获得ThreadLocal
        String token = tokens.get();

        //解锁
        Long result = (Long) redisTemplate.execute(new DefaultRedisScript(unLockLua, Long.class),
                Collections.singletonList(lockName),
                token);

        System.out.println("删除锁的结果:" + result);

        return result == 1;
    }
}

觉得楼主写的不错的点个关注和赞哟~