Redis分布式锁以及如何自研分布式锁

单机锁

// 单机加锁可以,但是分布式系统中会出现超卖现象 1.0
    public String sale() {
    	private Lock lock = new ReentrantLock();
        String retMessage = "";

        lock.lock();
        try {
            // 查询库存信息
            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;
    }

上述简单的例子的背景条件是在下完订单后去扣减库存的过程中
出现的问题:单机加锁会出现超卖现象,超卖指超出卖出商品的数量,本来100个线程进来,应该消耗100个商品,减少100个库存,但由于订单模块在不同的虚拟机,普通的加锁并不能满足需求,他们可能同时拿到相同的库存数量,然后减1,最后导致实际商品已经卖完,但库存显示还有

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

引入分布式锁,谁在redis上获取到锁,谁才能去访问库存

接下来的代码将从各个版本逐步升级,相关解说请看代码注释

2.0 引入分布式锁,递归重试可能会导致stackoverflowerror,另外,高并发唤醒后推荐用while判断而不是if

 // 2.0 引入分布式锁,递归重试可能会导致stackoverflowerror,另外,高并发唤醒后推荐用while判断而不是if
    public String sale() {
        String retMessage = "";
        String key = "lazyRedisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();

        // 分布式锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
        // 抢不到锁 递归重试
        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;
    }

升级V3.0

//3.0 使用自旋并加上过期时间
// 缺点: 由于加了过期时间,A线程加锁进来,但处理业务用了32s,这时锁过期了,
// B线程进来,也做同样的操作,执行到第2s的时候,A线程处理完后删除锁,这时删除的是B线程的锁,误删了
    public String sale() {
        String retMessage = "";
        String key = "lazyRedisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();

        // 不使用递归 改用自旋 抢不到锁就等待一会然后重试
        // 并且为了如果在删除锁的时候宕机导致key删除不了,需要加个过期时间
        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 {
                // 扣减成功后要删除锁,其他线程才能进来
                stringRedisTemplate.delete(key);
            }

        return retMessage + "\t" + "服务端口号" + port;
    }

升级 V4.0

// 4.0 改进了误删操作 但是这个操作不是原子性的
   public String sale() {
        String retMessage = "";
        String key = "lazyRedisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();

        // 不使用递归 改用自旋 抢不到锁就等待一会然后重试
        // 并且为了如果在删除锁的时候宕机导致key删除不了,需要加个过期时间
        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,不能删除别人的
            // 判断加锁与解锁是不是同一个客户端,同一个才行,自己只能删除自己的锁,不误删他人的
            if (stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)) {
                stringRedisTemplate.delete(key);
            }
        }

        return retMessage + "\t" + "服务端口号" + port;
    }

升级 V5.0

// 5.0 使用lua脚本改进删除操作,不满足可重入性
    public String sale() {
        String retMessage = "";
        String key = "lazyRedisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();

        // 不使用递归 改用自旋 抢不到锁就等待一会然后重试
        // 并且为了如果在删除锁的时候宕机导致key删除不了,需要加个过期时间
        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 {
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                    "then return redis.call('del',KEYS[1]) " +
                    "else " +
                    "return 0 " +
                    "end";
            stringRedisTemplate.execute(new DefaultRedisScript(script,Boolean.class), Arrays.asList(key),uuidValue);
        }

        return retMessage + "\t" + "服务端口号" + port;
    }

分布锁需要具备特征:独占性,高可用,防死锁,不乱抢,重入性
上述基本满足,但还没有考虑到可重入性问题,只是使用setnx 这个来实现分布式锁,真正要考虑到分布式锁需要使用hset 这种数据结构,以下代码将自研一把分布式锁,并考虑可重入性
要自研一把锁,那么
1、首先必须实现Lock接口并实现其中的方法
2、考虑可重入性
3、考虑续期问题
4、考虑可扩展性,使用设计模式的工厂模式,可以根据我们的需求使用不同的分布式锁,但以下代码是以redis分布式锁为例

建造一个工厂

/**
 * 使用工厂模式 根据不同的参数 调用不同的分布式锁
 *
 */
@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 = "lazyRedisLock";
            return new RedisDistributedLock(stringRedisTemplate, lockName, uuid);
        } else if (lockType.equalsIgnoreCase("ZOOKEEPER")) {
            this.lockName = "lazyZookeeperLockNode";
            //TODO zookeeper版本的分布式锁
            return null;
        } else if (lockType.equalsIgnoreCase("MYSQL")) {
            //TODO MYSQL版本的分布式锁
            return null;
        }
        return null;
    }
}

分布式锁实现

/**
 * 自研redis分布式锁
 */
public class RedisDistributedLock implements Lock {

    private StringRedisTemplate stringRedisTemplate;

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

    // 使用这个构造方法会导致每次都产生不同的uuid 删锁会报错
    /*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;
    }

    // 后台续期的监视线程
    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 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");
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

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

测试并使用

//V6.0版本,如何将我们的lock/unlock+lua脚本自研版的redis分布式锁搞定 满足可重入性
    // 重入性就是拿到一把锁后 再次拿同一把锁 无需重新加锁 只需记录锁的次数
    // 并且开启续期功能
    // 使用hset数据结构
    // 自研分布式锁工厂
    @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;
    }

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

V7.0,引入Redisson对应的官网推荐RedLock算法实现类

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

        RLock redissonLock = redisson.getLock("lazyRedisLock");
        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的简单配置

@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://127.0.0.1:6379")
                                .setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

以上是锁的升级过程以及心得,但关于红锁算法,看门狗策略这些知识看到时评论如何,可以的话会再更一期,上面的代码可以复制为两个模块,然后通过nginx代理进行访问测试,模拟多个机器访问库存模块

参考链接:视频链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值