手写Redis分布式锁

本次我们迭代了8个版本,由简单到复杂,由功能单一变功能强大,由单一性变为通用性。

V1.0 单机锁版本,先构建一个简单的单机锁,在此基础上逐步完善,直至形成一个分布式锁
改pom,写yaml,主启动,配置类等在此就不再赘述。直接开启业务类的编写。

初始版本,单机锁
@Service
@Slf4j
public class InventoryService {
    private Lock lock = new ReentrantLock();
    public String sale(){

        String message = "";
        lock.lock();
        String Value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();

        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //判断库存是否足够
            int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //库存足够,扣减库存
            if (inventoryNumber > 0 ) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));

                message = "成功卖出一件,剩余" + inventoryNumber ;
                System.out.println(Value);
                System.out.println(message);
            }else {
                message = "全部售罄!";
            }
        }finally {
            lock.unlock();
        }

        return message + "\t" + "服务端口号为: " + PORT;
    }
@RestController
@Slf4j
@Api(tags = "测试分布式锁")
public class InventoryController {

    @Resource private InventoryService inventoryService;


    @ApiOperation("扣减库存,卖一个")
    @GetMapping("/inventory/sale")
    public String sale(){
        return inventoryService.sale();
    }
}

一个单机锁的业务就完成了,在实际生产中,使用一个单机锁往往是不够的,会造成很多的问题,也限制了程序的扩展能力。
所以我们都是要使用分布式锁,那么使用分布式锁就需要考虑到线程安全的问题。

由此,我们再创建一个module,就直接先复制粘贴原来项目的pom,yaml,配置类,业务类吧,改动yaml中的端口为跟上一个不同就行。
此时我们在这个新的module中的InventoryService 类中添加一个synchronized或者lock/unlock锁。

V2.0 创建一个新的module,模拟分布式系统,启动nginx,使用nginx配置两个module,
在这里插入图片描述

@Service
@Slf4j
public class InventoryService {

    @Value("${server.port}") private String PORT;
    @Resource private StringRedisTemplate stringRedisTemplate;

    private Lock lock = new ReentrantLock();
    public String sale(){

        String message = "";
        String key = "inventory001";

        lock.lock();
        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get(key);
            //判断库存是否足够
            int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //库存足够,扣减库存
            if (inventoryNumber > 0 ) {
                stringRedisTemplate.opsForValue().set(key,String.valueOf(--inventoryNumber));

                message = "成功卖出一件,剩余" + inventoryNumber ;
                System.out.println(message);
            }else {
                message = "全部售罄!";
            }
        }finally {
            lock.unlock();
        }

        return message + "\t" + "服务端口号为: " + PORT;
    }
}

在这里插入图片描述
在主启动类中启动两个module,进行测试。
在浏览器进行测试http://192.168.10.100:/inventory/sale,一个一个点击时,nginx会默认轮询,可能不会出现问题,那我们就需要放出猛狗Jmeter进行压力测试!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如上图,2000个线程进行测试,测试我们发现,即使是两个module都加了锁,但是也发生了重复扣减库存的行为!那么我们由此得知,在分布式系统下,synchronized和lock锁都失效了!

在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程。

所以我们正式引入我们这篇的重点——分布式锁!
能干吗?!
1.跨进程+跨服务
2.解决超卖
3.防止缓存击穿

V3.1 引入分布式锁setnx

public String sale(){

        String message = "";
        String key = "MyRedisKey";
        String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value);
        if (!absent) {
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            sale();
        }else {

            try {
                //查询库存信息
                String result = stringRedisTemplate.opsForValue().get("inventory001");
                //判断库存是否足够
                int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //库存足够,扣减库存
                if (inventoryNumber > 0 ) {
                    stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));

                    message = "成功卖出一件,剩余" + inventoryNumber ;
                    System.out.println(message);
                }else {
                    message = "全部售罄!";
                }
            }finally {
                //一定要记住删除key!
                stringRedisTemplate.delete(key);
            }
        }

        return message + "\t" + "服务端口号为: " + PORT;
    }

V3.2 上个版本,使用if递归重试的方式,但是在高并发下,容易造成线程的虚假唤醒,造成stackOverFlowError,不太推荐。所以我们可以使用while自旋来替代if判断的递归。

//V3.2  使用while自旋来替代if判断的递归,避免栈溢出
    public String sale(){

        String message = "";
        String key = "MyRedisKey";
        String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value);
        while (!absent) {
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
            try {
                //查询库存信息
                String result = stringRedisTemplate.opsForValue().get("inventory001");
                //判断库存是否足够
                int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //库存足够,扣减库存
                if (inventoryNumber > 0 ) {
                    stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));

                    message = "成功卖出一件,剩余" + inventoryNumber ;
                    System.out.println(message);
                }else {
                    message = "全部售罄!";
                }
            }finally {
                //一定要记住删除key!
                stringRedisTemplate.delete(key);
            }

        return message + "\t" + "服务端口号为: " + PORT;
    }

V4.1 在3.2版本结束后,我们又要考虑到,万一程序在运行中突然宕机,最后的 stringRedisTemplate.delete(key);都没进行到,那么这不是造成了一个死锁?所以我们需要加入一个过期时间,哪怕程序宕机了,也不至于出现程序被锁住的尴尬情况

//V4.1  设置过期时间防止死锁
    public String sale(){

        String message = "";
        String key = "MyRedisKey";
        String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value);
        while (!absent) {
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        stringRedisTemplate.expire(key,30L,TimeUnit.SECONDS);
        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //判断库存是否足够
            int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //库存足够,扣减库存
            if (inventoryNumber > 0 ) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));

                message = "成功卖出一件,剩余" + inventoryNumber ;
                System.out.println(message);
            }else {
                message = "全部售罄!";
            }
        }finally {
            //一定要记住删除key!
            stringRedisTemplate.delete(key);
        }

        return message + "\t" + "服务端口号为: " + PORT;
    }

V4.2 可以由上述的4.1版本代码看到,setnx设置锁与设置锁的过期时间expire,不在一行上,那么在高并发情况下,如果某一次进行到这两行代码之间的代码,突然造成了宕机,还没进行到设置过期时间这行,不还是没设置过期时间吗,这就又造成了死锁。
在这种情况下,跟V3.2的代码没什么区别,所以我们要保证原子性,将他们合并成一行。

public String sale(){

        String message = "";
        String key = "MyRedisKey";
        String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key, Value,30L,TimeUnit.SECONDS);
        while (!absent) {
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //判断库存是否足够
            int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //库存足够,扣减库存
            if (inventoryNumber > 0 ) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));

                message = "成功卖出一件,剩余" + inventoryNumber + Value;
                System.out.println(message);
            }else {
                message = "全部售罄!";
            }
        }finally {
            //一定要记住删除key!
            stringRedisTemplate.delete(key);
        }

        return message + "\t" + "服务端口号为: " + PORT;
    }

使用jmeter压测,在这里插入图片描述
redis查询表示程序通过。

V5.0 防止误删key,只允许删除自己的key,不允许删除别人的key!例如A先抢到线程,A开始工作,过期时间为30秒,但是A到30秒还没干完,没干完就被删了!此时B发现redis数据库中key已经空了,抢占到线程,开始工作,但是此时A还在执行代码,2秒以后,A执行完了,在finally中删除key,那么此时的key是B的key啊!不是A的key,造成了误删。
张冠李戴,删除了别人的key

 public String sale(){

        String message = "";
        String key = "MyRedisLockKey";
        String Value = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(key,Value,30L,TimeUnit.SECONDS);
        while (!absent) {
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //判断库存是否足够
            int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //库存足够,扣减库存
            if (inventoryNumber > 0 ) {
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));

                message = "成功卖出一件,剩余" + inventoryNumber + "\t" + Value;
                System.out.println(Thread.currentThread().getId());
                System.out.println(message);
            }else {
                message = "全部售罄!";
            }
        }finally {
         // v5.0判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(Value)) {
                stringRedisTemplate.delete(key);
            }
        }

        return message + "\t" + "服务端口号为: " + PORT;
    }

V6.0 在4.2版本中,我们讨论了设置过期时间与加锁在同一行的问题,这样保证了程序的原子性,保证了程序宕机时,不会出现问题。
所以在5.0版本时,我们可以发现,是不是同一把锁的判断与删除操作也不是同一行(不是同一个命令),这就有可能在高并发下造成错误。所以我们要保证程序的原子性。这时我们就需要使用lua来保证原子性。
关于lua脚本,请搜索lua教程。这里不做太多的讨论。

// V6.0      删除key操作,使用lua脚本删除,保证其原子性,在高并发下代码性能更好
    public String sale(){
        String returnMessage = "";

        String key = "MyRedisLockKey";
        String value = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        while (!stringRedisTemplate.opsForValue().setIfAbsent(key, value, 30L, TimeUnit.SECONDS)) {
            try {
                TimeUnit.MILLISECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //获取库存数
            int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //库存足够,扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));

                returnMessage = "库存扣减成功,剩余库存:" + inventoryNumber + "\t" + value;
                System.out.println(returnMessage);
            }else {
                returnMessage = "全部售罄!";
            }
        }finally {
//            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(value)) {
//                stringRedisTemplate.delete(key);
//            }
			 //V6.0 将判断+删除自己的合并为lua脚本保证原子性
            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),value);
        }
        return returnMessage + "\t" + PORT;
    }

V7.0 到这一步,我们已经清楚的知道,我们还需要一个可重入的锁,允许同一个线程多次获取同一把锁。那么reentrant或者synchronized就实现了可重入性呀!
但是!我们之前已经讲过,reentrant或者synchronized只能在单个JVM实例下有用,在分布式系统下就失效了,而我们现在就是个分布式系统。
所以我们需要自己手写实现在分布式下的可重入锁。

那么思考一下,redis中什么类型的数据结构可以满足实现可重入锁呢?
那就是HSET,可以实现加锁的次数,解锁时又可以将加了锁的数值减1。如果为0,那就是没有锁了。
在这里插入图片描述
小总结:setnx可以解决有无分布式锁的问题,hset不但解决了分布式锁的问题,还实现了可重入的问题。

介绍完毕。现在我们开始着手V7.1的编写。
要实现以上的功能,就要编写lua脚本,用lua脚本加锁解锁,以保证原子性。

lua脚本加锁的逻辑:
在这里插入图片描述
加锁的lua脚本:
在这里插入图片描述
hincrby可以替代hset命令,所以合并一下可得:
可以在redis客户端中测试,这里就不再测试
lua脚本解锁的逻辑:
在这里插入图片描述
解锁的lua脚本:
可以在redis客户端中测试,这里就不再测试

测试在redis客户端中使用lua加锁解锁全套流程
上述前提工作完成后,我们就可以编写一个自己的Lock,实现可重入的分布式锁了。
我们要了解的是,我们这个锁也是对Lock锁的接口规范定义来进行代码的落地。

V7.0 通过实现JUC里面的Lock接口,实现redis分布式锁RedisDistributedLock

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();//UUID:ThreadID
        this.expireTime = 30L;
    }
    @Override
    public void lock()
    {
        tryLock();
    }
    @Override
    public boolean tryLock()
    {
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    /**
     * 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
        if(time != -1L){
            this.expireTime = unit.toSeconds(time);
        }
        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("script: "+script);
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);

        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
        return true;
    }

    /**
     *干活的,实现解锁功能
     */
    @Override
    public void unlock()
    {
        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
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("This lock doesn't EXIST");
        }

    }

    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }

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

InventoryService 类中原来使用setnx来加锁的代码块就可以替换成我们自己的Lock了。

@Service
@Slf4j
public class InventoryService {

    @Value("${server.port}") private String PORT;
    @Resource private StringRedisTemplate stringRedisTemplate;
    private Lock myRedisDistributedLock = new MyRedisDistributedLock(stringRedisTemplate,"MyRedisLock");


    // V7.0  lua脚本结合hset考虑锁的可重入性
    public String sale(){
        String returnMessage = "";

        myRedisDistributedLock.lock();
       try {
            //查询库存信息
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //获取库存数
            int inventoryNumber = result == null ? 0 : Integer.parseInt(result);
            //库存足够,扣减库存
            if (inventoryNumber > 0) {
                stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));

                returnMessage = "库存扣减成功,剩余库存:" + inventoryNumber + "\t";
                System.out.println(returnMessage);
            }else {
                returnMessage = "全部售罄!";
            }
        }finally {
           myRedisDistributedLock.unlock();
        }
        return returnMessage + "\t" + PORT;
    }

但其实到这里还是有点小问题的,就是这个我们自己写的Lock已经被写死了,我不具备扩展性和通用性,只能供给Redis一个使用。当zookeeper或者mysql需要分布式锁,那么就需要再加,所以为了扩展性和通用性,我们引入工厂模式来完善。
V7.1 引入工厂模式完善代码,提升扩展性和通用性
DistributedLockFactory:

@Component
public class DistributedLockFactory
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;

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

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

        return null;
    }
}

RedisDistributedLock :

//@Component 引入DistributedLockFactory工厂模式,从工厂获得而不再从spring拿到
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();//UUID:ThreadID
        this.expireTime = 30L;
    }
    @Override
    public void lock(){
        tryLock();
    }
    @Override
    public boolean tryLock(){
        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}
        return false;
    }

    /**
     * 干活的,实现加锁功能,实现这一个干活的就OK,全盘通用
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException{
        if(time != -1L){
            this.expireTime = unit.toSeconds(time);
        }
        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("script: "+script);
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
            TimeUnit.MILLISECONDS.sleep(50);
        }
        return true;
    }

    /**
     *干活的,实现解锁功能
     */
    @Override
    public void unlock()
    {
        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
        System.out.println("lockName: "+lockName);
        System.out.println("uuidValue: "+uuidValue);
        System.out.println("expireTime: "+expireTime);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("This lock doesn't EXIST");
        }

    }

    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    //===下面的redis分布式锁暂时用不到=======================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }

    @Override
    public Condition newCondition()
    {
        return null;
    }
}
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @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)
            {
                inventoryNumber = inventoryNumber - 1;
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(inventoryNumber));
                retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber+"\t服务端口:" +port;
                System.out.println(retMessage);
                return retMessage;
            }
            retMessage = "商品卖完了,o(╥﹏╥)o"+"\t服务端口:" +port;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisLock.unlock();
        }
        return retMessage;
    }
}

V7.1 改造完成,进行测试。
在这里插入图片描述
在这里插入图片描述

V7.1版本下,我们仅仅是写了一个我们自己的分布式锁,RedisDistributedLock 以及引入了RedisDistributedFactory工厂模式,并没有测试可重入性。所以我们在V7.1下测试可重入性。
在inventoryService类中,新增一个方法,此方法里面加锁解锁,看看进入该方法的是不是同一个。

@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @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+"\t";
                System.out.println(retMessage);
                //新增的方法,测试进出该方法的是不是同一把锁
                testReEnter();
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }

    private void testReEnter()
    {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            System.out.println("################测试可重入锁#######");
        }finally {
            redisLock.unlock();
        }
    }
}

public class MyRedisDistributedLock implements Lock {


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

    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long expireTime;
    @Override
    public void lock() {
        tryLock();
    }


    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L) {
        //打印加锁的uuid,与解锁的uuid进行对比,查看另一个加了锁又解锁的的方法的锁与原本的锁是否是同一个,因为是同一个才能保证重入性
            System.out.println("lock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
            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";
            while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
                    Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                TimeUnit.MILLISECONDS.sleep(60);
            }
            return true;
        }
        return false;
    }

    @Override
    public void unlock() {
		//打印加锁的uuid,与解锁的uuid进行对比,查看另一个加了锁又解锁的的方法的锁与原本的锁是否是同一个,因为是同一个才能保证重入性
        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";

        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(lockName), uuidValue);
        if (flag == null) {
            throw new RuntimeException("this lock doesn't exists!!");
        }
    }


    @Override
    public void lockInterruptibly() throws InterruptedException {

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

测试结果:不是同一个锁,可重入性还没实现。
在这里插入图片描述
ThreadId一致,但是uuid不一致,为什么?
引出V7.2版本
DistributedLockFactory:
在这里插入图片描述

public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long   expireTime;

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        //注意:这里原本的IdUtil.simpleUUID()就改为从工厂传来的uuidValue了
        this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }

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

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        if(time != -1L)
        {
            expireTime = unit.toSeconds(time);
        }
		 System.out.println("lock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
        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)))
        {
            try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        return true;
    }

    @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";
        System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("没有这个锁,HEXISTS查询无");
        }
    }

    //=========================================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }
    @Override
    public Condition newCondition()
    {
        return null;
    }
}
@Service
@Slf4j
public class InventoryService
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;
    @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);
                this.testReEnter();
            }else{
                retMessage = "商品卖完了,o(╥﹏╥)o";
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            redisLock.unlock();
        }
        return retMessage+"\t"+"服务端口号:"+port;
    }


    private void testReEnter()
    {
        Lock redisLock = distributedLockFactory.getDistributedLock("redis");
        redisLock.lock();
        try
        {
            System.out.println("################测试可重入锁####################################");
        }finally {
            redisLock.unlock();
        }
    }
}

使用jmeter进行测试,单机+并发+可重入性都通过。如图所示:
在这里插入图片描述
到这里,我们还需考虑业务执行时间的问题,尽管我们所设置的过期时间已经足够业务执行完毕,但是在使用中如果碰到拥堵,一个线程执行业务的时间远远大于线程的过期时间了,怎么办?
V8.0 新增自动续期功能
在这里插入图片描述
那么可以设置一个很大的过期时间吗?不行,考虑到系统的健壮性以及扩展性,还是不要写死这个时间,那么我们就需要引入V8.0版本——自动续期。
使用lua脚本
注意:在java代码块中编写lua脚本,lua脚本的字符串换行时,要在每行最后一个字符后面加空格,不然会报错误。很多初学者可能会忽略这个细节,造成错误。
在这里插入图片描述

在这里插入图片描述

public class MyRedisDistributedLock implements Lock {


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

    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long expireTime;
    @Override
    public void lock() {
        tryLock();
    }


    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L) {
            System.out.println("lock():lockName:" + lockName + "\t" + "uuidValue:" + uuidValue);
            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";
            while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
                    Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                TimeUnit.MILLISECONDS.sleep(60);
            }
            //自动加锁功能
            this.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";

        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(lockName), uuidValue);
        if (flag == null) {
            throw new RuntimeException("this lock doesn't exists!!");
        }
    }

	//自动加锁,new Timer在JUC中有涉及,只一个java 的定时器
	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;
    }
}

在MyRedisDistributedLock类中的添加了一个自动续期的方法,这个方法中有一个new Timer,在JUC中有涉及,感兴趣的可以看看JUC。
new timer是一个java的定时器任务,schedule方法可以添加计时器任务,以及多久的延迟。
在这里插入图片描述
完成上述自动续期的功能后,我们去测试,此时在InventoryService中人为的sleep一段时间,看看是否会触发自动续期。
在这里插入图片描述
到这里,8个版本就完成了,redis分布式锁的整体功能都已经完善了,其中细节还需补充,例如此时的锁是一个单机锁,如果碰到了严重的单点故障问题,导致这个单机的redis分布式锁挂了怎么办?
你可能想到,创建锁集群,这个挂了其他的补上!但通常这是不行的。原因如图所示:
在这里插入图片描述
那么如何解决呢?请看另一篇《Redlock算法和底层源码分析》

  • 32
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值