文章目录

  • 锁的种类
  • 一个靠谱分布式锁需要具备的条件和刚需
  • 独占性
  • 高可用
  • 防死锁
  • 不乱抢
  • 重入性
  • 如何一步一步实现一个完备的分布式锁
  • 单机版加锁
  • Redis分布式锁setnx
  • 宕机与过期 + 防死锁
  • 防止误删key的问题
  • lua脚本保证原子性
  • hsetnx 可重入锁+简单工厂模式
  • RedisDistributeLock
  • DistributedLockFactory
  • 自动续期
  • CAP
  • 小结 *
  • RedLock
  • 为什么需要它
  • Redlock设计理念
  • 如何使用
  • RedisConfig
  • saleByRedisson
  • 源码简单分析


锁的种类

单机版同一个JVM虚拟机内:synchronized或者Lock接口。
分布式多个不同JVM虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

一个靠谱分布式锁需要具备的条件和刚需

独占性

任何时候只能有一个线程持有

高可用

若redis集群环境下,不能因为某个节点挂了而出现获取锁和释放锁失败的情况
高并发请求下,依旧性能足够

防死锁

杜绝死锁,必须要有超时控制机制或者撤销操作,有个兜底终止跳出方案

不乱抢

防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放

重入性

同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁

如何一步一步实现一个完备的分布式锁

分布式锁用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用

使用场景:多个服务间保证同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击),类似超卖。

redis分布式锁_端口号


redis分布式锁_redis_02

单机版加锁

  • 为什么单机版加了锁还没有控制住?
    在单机环境下,可以使用synchronized或Lock来实现。
    但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建),不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
  • 分布式锁能干嘛
    可以跨进程,跨服务;解决超卖;防止缓存击穿;
    单机版加锁配合Nginx和Jmeter压测后,不满足高并发分布式锁的性能要求,出现超卖
private Lock lock = new ReentrantLock();
    public String sale()
    {
        String retMessage = "";

        lock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存,每次减少一个
            if(inventoryNumber > 0)
            {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            lock.unlock();
        }
        return retMessage+"\t"+"服务端口号"+port;
    }

Redis分布式锁setnx

Redis具有极高的性能,且它的命令对分布式锁支持友好,借助SET命令即可实现加锁处理。
下面代码出现的问题:
递归重试,容易导致stackoverflowerror,所以不太推荐;另外,高并发唤醒后推荐用while判断而不是if

public String sale()
    {
        String retMessage = "";
        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
        //flag=false,抢不到的线程要继续重试。。。。。。
        if(!flag)
        {
            //暂停20毫秒,进行递归重试.....
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
            sale();
        }else{
            //抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
            try
            {
                //1 查询库存信息
                String result = stringRedisTemplate.opsForValue().get("inventory001");
                //2 判断库存是否足够
                Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //3 扣减库存,每次减少一个
                if(inventoryNumber > 0)
                {
                    stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                    retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                    System.out.println(retMessage+"\t"+"服务端口号"+port);
                }else{
                    retMessage = "商品卖完了,o(╥﹏╥)o";
                }
            }finally {
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage+"\t"+"服务端口号"+port;
      }

下面代码采用自旋锁代替递归重试:
下面代码问题:
部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定key。

redis分布式锁_分布式锁_03

public String sale()
    {
        String retMessage = "";

        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        //不用递归了,高并发下容易出错,我们用自旋替代递归方法重试调用;也不用if了,用while来替代
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue))
        {
            //暂停20毫秒,进行重试.....
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        //抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存,每次减少一个
            if(inventoryNumber > 0)
            {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage+"\t"+"服务端口号"+port;
    }

宕机与过期 + 防死锁

下面代码设置了过期时间,并且合并成了一行,具备原子性
下面代码存在问题:
stringRedisTemplate.delete(key);只能自己删除自己的锁,不可以删除别人的,需要添加判断是否是自己的锁来进行操作

public String sale()
    {
        String retMessage = "";

        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        //改进点:加锁和过期时间设置必须同一行,保证原子性
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS))
        {
            //暂停20毫秒,进行重试.....
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        //stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);

        //抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存,每次减少一个
            if(inventoryNumber > 0)
            {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            stringRedisTemplate.delete(key);
        }
        return retMessage+"\t"+"服务端口号"+port;
    }

防止误删key的问题

存在问题:
最后的判断+del不是一行原子命令操作,需要用lua脚本进行修改

public String sale()
    {
        String retMessage = "";

        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        while(!stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS))
        {
            //暂停20毫秒,进行递归重试.....
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        //抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存,每次减少一个
            if(inventoryNumber > 0)
            {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            //改进点,只能删除属于自己的key,不能删除别人的
            // v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
            if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue))
            {
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage+"\t"+"服务端口号"+port;
    }

lua脚本保证原子性

什么是lua脚本?

设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Redis调用Lua脚本通过eval命令保证执行的原子性,直接用return返回脚本执行后的结果值。

redis分布式锁_分布式_04

redis分布式锁_jvm_05


redis分布式锁_分布式锁_06


redis分布式锁_jvm_07


redis分布式锁_分布式锁_08


redis分布式锁_jvm_09


LUA脚本条件判断语法:

redis分布式锁_jvm_10


redis分布式锁_分布式_11

存在问题:
不满足可重入性,需要重新修改

public String sale()
    {
        String retMessage = "";

        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        while(!stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS))
        {
            //暂停20毫秒,进行递归重试.....
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        //redislock();
        //抢锁成功的请求线程,进行正常的业务逻辑操作,扣减库存
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存,每次减少一个
            if(inventoryNumber > 0)
            {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
                testReEnter();
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            //unredislock();
            //改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
            String luaScript =
                    "if redis.call('get',KEYS[1]) == ARGV[1] then " +
                        "return redis.call('del',KEYS[1]) " +
                    "else " +
                        "return 0 " +
                    "end";
            stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);
        }
        return retMessage+"\t"+"服务端口号"+port;
    }

    private void testReEnter()
    {
       *//* String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        while(!stringRedisTemplate.opsForValue().setIfAbsent(key,uuidValue,30L,TimeUnit.SECONDS))
        {
            //暂停20毫秒,进行递归重试.....
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        redislock();
        //biz......
        unredislock();
        //改进点,修改为Lua脚本的redis分布式锁调用,必须保证原子性,参考官网脚本案例
        String luaScript =
                "if redis.call('get',KEYS[1]) == ARGV[1] then " +
                        "return redis.call('del',KEYS[1]) " +
                        "else " +
                        "return 0 " +
                        "end";
        stringRedisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),uuidValue);*//*
    }

hsetnx 可重入锁+简单工厂模式

以上方式达到了分布式锁要求的独占性,高可用,防死锁,不乱抢,但是没有重入性。
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
总结一句话就是一个线程的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。(自己可以获取自己的内部锁)
问题:

setnx只能解决锁的有无问题,够用但不完美。

hset不但解决有无,还解决可重入问题。

将lock和unlock方法进行改进,把它们全部变成lua脚本。

redis分布式锁_分布式锁_12


加锁的lua脚本:

redis分布式锁_redis_13


redis分布式锁_分布式_14


解锁的lua脚本:

redis分布式锁_jvm_15

RedisDistributeLock

package com.atguigu.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @auther zzyy
 * @create 2023-01-09 16:41
 * 我们自研的redis分布式锁,实现了Lock接口
 */
//@Component 引入DistributedLockFactory工厂模式,从工厂获得即可
public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;

    private String lockName;//KEYS[1]
    private String uuidValue;//ARGV[1]
    private long   expireTime;//ARGV[2]

    /*public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();
        this.expireTime = 25L;
    }*/

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuid+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }

    @Override
    public void lock()
    {
        tryLock();
    }
    @Override
    public boolean tryLock()
    {
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        if(time == -1L)
        {
            String script =
                    "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then    " +
                            "redis.call('hincrby',KEYS[1],ARGV[1],1)    " +
                            "redis.call('expire',KEYS[1],ARGV[2])    " +
                            "return 1  " +
                    "else   " +
                            "return 0 " +
                    "end";
            System.out.println("lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);

            while(!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime)))
            {
                //暂停60毫秒
                try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
            }
            //新建一个后台扫描程序,来坚持key目前的ttl,是否到我们规定的1/2 1/3来实现续期
            renewExpire();
            return true;
        }
        return false;
    }


    @Override
    public void unlock()
    {
        System.out.println("unlock(): lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then    " +
                        "return nil  " +
                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then    " +
                        "return redis.call('del',KEYS[1])  " +
                "else    " +
                        "return 0 " +
                "end";
        // nil = false 1 = true 0 = false
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));

        if(null == flag)
        {
            throw new RuntimeException("this lock doesn't exists,o(╥﹏╥)o");
        }
    }
//自动续期:确保redisLock过期时间大于业务执行时间的问题
    private void renewExpire()
    {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then     " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                "else     " +
                        "return 0 " +
                "end";

        new Timer().schedule(new TimerTask()
        {
            @Override
            public void run()
            {
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
                {
                    renewExpire();
                }
            }
        },(this.expireTime * 1000)/3);
    }



    //====下面两个暂时用不到,不再重写
    //====下面两个暂时用不到,不再重写
    //====下面两个暂时用不到,不再重写
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }
    @Override
    public Condition newCondition()
    {
        return null;
    }
}

DistributedLockFactory

package com.atguigu.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.Lock;

/**
 * @auther zzyy
 * @create 2023-01-09 17:28
 */
@Component
public class DistributedLockFactory
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuid;

    public DistributedLockFactory()
    {
        this.uuid = IdUtil.simpleUUID();
    }

    public Lock getDistributedLock(String lockType)
    {
        if(lockType == null) return null;

        if(lockType.equalsIgnoreCase("REDIS")){
            this.lockName = "zzyyRedisLock";
            return new RedisDistributedLock(stringRedisTemplate,lockName,uuid);
        }else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
            this.lockName = "zzyyZookeeperLockNode";
            //TODO zookeeper版本的分布式锁
            return null;
        }else if(lockType.equalsIgnoreCase("MYSQL")){
            //TODO MYSQL版本的分布式锁
            return null;
        }

        return null;
    }
}
@Autowired
private DistributedLockFactory distributedLockFactory;
public String sale()
    {
        String retMessage = "";

        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存,每次减少一个
            if(inventoryNumber > 0)
            {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
                testReEntry();
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号"+port;
    }

    private void testReEntry()//用在V7.0版本程序作为测试可重入性
    {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            System.out.println("===========测试可重入锁========");
        }finally {
            redisLock.unlock();
        }
    }

自动续期

为了确保redisLock过期时间大于业务执行时间,需要给锁续期。

CAP

AP:Redis集群
redis异步复制造成的锁丢失,主节点没来的及把刚刚set进来这条数据给从节点,主机挂了,从机上位但无该数据
CP: Zookeeper集群
具体不了解了

小结 *

  • 第一代:synchronized 单机版OK,上分布式就混乱了,无法使用nginx分布式微服务
  • 第二代:取消单机锁,上redis分布式锁setnx:
    只加了锁,没有释放锁,出现异常的话,可能就无法释放锁,必须代码层面在finally处释放锁;
    宕机了,部署了微服务代码层面,根本没有走到finally这块,无法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定;
    为redis的分布式锁key增加过期时间,并且需要setnx+过期时间必须同一行,必须规定只能自己删除自己的锁,防止张冠李戴把别人的锁删除了,unlock变为LUA脚本保证原子性,锁重入方面,使用hset代替setnx+lock变为Lua脚本保证,最后再增加一个key的自动续期。

RedLock

为什么需要它

线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点,在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;redis 触发故障转移,其中一个 slave 升级为新的 master,此时新上位的master并不包含线程1写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁,此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。

我们加的是排它独占锁,同一时间只能有一个建redis锁成功并持有锁,严禁出现2个以上的请求线程拿到锁。危险的

redis分布式锁_端口号_16

Redlock设计理念

Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。

锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。

为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功。

条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;

条件2:客户端获取锁的总耗时没有超过锁的有效时间。

使用容错公式解决:

redis分布式锁_redis_17

如何使用

RedisConfig

package com.atguigu.redislock.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @auther zzyy
 * @create 2023-01-04 15:58
 */
@Configuration
public class RedisConfig
{
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public Redisson redisson()
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.185:6379").setDatabase(0).setPassword("111111");

        return (Redisson) Redisson.create(config);
    }
}

saleByRedisson

@Autowired
    private Redisson redisson;
    public String saleByRedisson()
    {
        String retMessage = "";

        RLock redissonLock = redisson.getLock("zzyyRedisLock");
        redissonLock.lock();

        try
        {
            //1 查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 判断库存是否足够
            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //3 扣减库存,每次减少一个
            if(inventoryNumber > 0)
            {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
                System.out.println(retMessage+"\t"+"服务端口号"+port);
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }finally {
            //改进点,只能删除属于自己的key,不能删除别人的
            if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
            {
                redissonLock.unlock();
            }
        }
        return retMessage+"\t"+"服务端口号"+port;
    }

源码简单分析

如果Redis分布式锁过期了,但是业务逻辑没处理完怎么办?
额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;
在获取锁成功后,给锁加一个watchdog,watchdog会起一个定时任务,在锁没有被释放且快要过期的时候会续期。

通过Redission新建出来的锁key,默认是30秒。

redis分布式锁_jvm_18


redis分布式锁_分布式_19


加锁:

redis分布式锁_分布式_20


redis分布式锁_分布式锁_21


看门狗:

redis分布式锁_redis_22

这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。

在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。

客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始。

redis分布式锁_端口号_23


redis分布式锁_jvm_24


解锁:

redis分布式锁_jvm_25