• 基于Redis实现分布式锁


    我们知道分布式锁的特性是排他、避免死锁、高可用。分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update)、Redis的setnx()命令、Zookeeper(在某个持久节点添加临时有序节点,判断当前节点是否是序列中最小的节点,如果不是则监听比当前节点还要小的节点。如果是,获取锁成功。当被监听的节点释放了锁(也就是被删除),会通知当前节点。然后当前节点再尝试获取锁,如此反复)

     
    redis.png

    本篇文章,主要讲如何用Redis的形式实现分布式锁。后续文章会讲解热点KEY读取,缓存穿透和缓存雪崩的场景和解决方案、缓存更新策略等等知识点,理论知识点较多。

    Redis配置

    spring:
      redis:
        port: 6379
        database: 0
        host: 127.0.0.1
        password:
        jedis:
          pool:
            max-active: 8
            max-wait: -1ms
            max-idle: 8
            min-idle: 0
        timeout: 5000ms 

    Jedis工具类

    public class JedisConnectionUtil {
    
        private static JedisPool pool = null;
    
        static {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(100);
            pool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379);
        }
    
        public static Jedis getJedis(){
            return pool.getResource();
        }
    }
    

      

    分布式锁分析与编码

    下面进入正文。因为分布式系统之间是不同进程的,单机版的锁无法满足要求。所以我们可以借助中间件Redis的setnx()命令实现分布式锁。setnx()命令只会对不存在的key设值,返回1代表获取锁成功。对存在的key设值,会返回0代表获取锁失败。这里的value是System.currentTimeMillis() (获取锁的时间)+锁持有的时间。我这里设置锁持有的时间是200ms,实际业务执行的时间远比这200ms要多的多,持有锁的客户端应该检查锁是否过期,保证锁在释放之前不会过期。因为客户端故障的情况可能是很复杂的。比如现在有A,B俩个客户端。A客户端获取了锁,执行业务中做了骚操作导致阻塞了很久,时间应该远远超过200ms,当A客户端从阻塞状态下恢复继续执行业务代码时,A客户端持有的锁由于过期已经被其他客户端占有。这时候A客户端执行释放锁的操作,那么有可能释放掉其他客户端的锁。

    我这里设置的客户端等待锁的时间是200ms。这里通过轮询的方式去让客户端获取锁。如果客户端在200ms之内没有锁的话,直接返回false。实际场景要设置合适的客户端等待锁的时间,避免消耗CPU资源。

    获取锁

    /**
     * 获取锁
     * @param lockName 锁的名字
     * @param acquireTimeout 或得所的超时时间
     * @param lockTimeout 所本身的超时时间
     * @return
     */
    public String acquireLock(String lockName, long acquireTimeout, long lockTimeout){
        // 锁标识
        String identifler = UUID.randomUUID().toString();
        String lockKey = "lock" + lockName;
        int lockExpire = (int) (lockTimeout / 1000);
        Jedis jedis = null;
    
        try {
            jedis = JedisConnectionUtil.getJedis();
    
            long end = System.currentTimeMillis() + acquireTimeout;
            // 获取锁的限定时间
            while (System.currentTimeMillis() < end) {
                if (jedis.setnx(lockKey, identifler) == 1) {
                    // 设置成功
                    // 设置超时时间
                    jedis.expire(lockKey, lockExpire);
                    // 或得锁成功
                    return identifler;
                }
                if (jedis.ttl(lockKey) == -1) {
                    // 如果一直没有或得锁设置超时时间
                    jedis.expire(lockKey, lockExpire);
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            jedis.close();
        }
        return null;
    }
    

      

    所以,我们要加上锁过期,然后获取锁的策略。通过realKey获取当前的currentValue。currentValue也就是获取锁的时间 + 锁持有的时间。 如果currentValue不等于null 且 currentValue 小于当前时间,说明锁已经过期。这时候如果突然来了C,D两个客户端获取锁的请求,不就让C,D两个客户端都获取锁了吗。如果防止这种现象发生,我们采用getSet()命令来解决。getSet(key,value)的命令会返回key对应的value,然后再把key原来的值更新为value。也就是说getSet()返回的是已过期的时间戳。如果这个已过期的时间戳等于currentValue,说明获取锁成功。
     

    释放锁

    /**
     * 释放锁
     * @param lockName 锁的名字
     * @param identifler 锁标识
     * @return
     */
    public boolean releaseLock(String lockName, String identifler){
        System.out.println(lockName + "释放锁" + identifler);
        String lockKey = "lock:" + lockName;
        Jedis jedis = null;
        boolean isrelease = false;
        try {
            jedis = JedisConnectionUtil.getJedis();
            while (true){
                jedis.watch(lockKey);
                // 判断是否是同一把锁
                if (identifler.equals(jedis.get(lockKey))){
                    Transaction transaction = jedis.multi();
                    transaction.del(lockKey);
                    if (transaction.exec().isEmpty()){
                        continue;
                    }
                    isrelease = true;
                }
                // TODO 异常
                jedis.unwatch();
                break;
            }
        }finally {
            jedis.close();
        }
        return isrelease;
    }
    

    编写测试类用多线程模拟高并发

    public class UnitTest extends Thread {
    
        @Override
        public void run() {
            while (true){
                DistributedLock distributedLock = new DistributedLock();
                String rs = distributedLock.acquireLock("updateOrder", 2000, 5000);
                if (rs != null){
                    System.out.println(Thread.currentThread().getName() + "-> 或得锁" + rs);
                    try {
                        Thread.sleep(1000);
                        distributedLock.releaseLock("updateOrder", rs);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    break;
                }
            }
        }
    
        public static void main(String[] args) {
            UnitTest unitTest = new UnitTest();
            for (int i = 0; i < 10; i++) {
                new Thread(unitTest, "tName:" + i).start();
            }
        }
    }
    

    代码运行结果

    tName:0-> 或得锁a86a2867-a5f9-4b81-8be6-105d50b8a1ab
    updateOrder释放锁a86a2867-a5f9-4b81-8be6-105d50b8a1ab
    tName:1-> 或得锁bcdeb19c-a47c-4a12-86df-8e5d90b2ecb0
    updateOrder释放锁bcdeb19c-a47c-4a12-86df-8e5d90b2ecb0
    tName:6-> 或得锁147d9fc8-5e15-4e86-8da6-e029a3e2d92f
    updateOrder释放锁147d9fc8-5e15-4e86-8da6-e029a3e2d92f
    tName:2-> 或得锁93deb41d-2439-45e7-beb1-2e4ce672e8ca
    updateOrder释放锁93deb41d-2439-45e7-beb1-2e4ce672e8ca
    tName:4-> 或得锁094f921d-fe9b-46ba-873b-aee2ce974f16
    updateOrder释放锁094f921d-fe9b-46ba-873b-aee2ce974f16
    tName:5-> 或得锁216e0799-6d22-4ae4-bb83-9efe1e5f80c9
    updateOrder释放锁216e0799-6d22-4ae4-bb83-9efe1e5f80c9
    tName:9-> 或得锁678e2099-651c-4e23-a648-9fb588ecb42b
    updateOrder释放锁678e2099-651c-4e23-a648-9fb588ecb42b
    tName:7-> 或得锁f35cdbad-6fde-4f1e-a4c1-321805a39374
    updateOrder释放锁f35cdbad-6fde-4f1e-a4c1-321805a39374
    tName:3-> 或得锁0913a4a0-805a-48e2-ac5a-c0762b8c4072
    updateOrder释放锁0913a4a0-805a-48e2-ac5a-c0762b8c4072
    tName:8-> 或得锁a3964a2a-6b9c-4ca3-9ea5-53de854c23ce
    updateOrder释放锁a3964a2a-6b9c-4ca3-9ea5-53de854c23ce
     
     
     
    本文文字部分参考文章:https://www.jianshu.com/p/83224c0f3bb9
  • 相关阅读:
    Icident event 分析
    innodb buffer pool相关特性
    备库Seconds_Behind_Master的计算
    savepoint原理
    layer探框
    解决layui表格和下拉框同时使用时,下拉框被表格遮当问题
    layui switch开关按钮
    formSelects 4.x多选下拉框
    java如何调用php接口,并获取值
    layui时间范围选择器
  • 原文地址:https://www.cnblogs.com/lyze/p/11800792.html
Copyright © 2020-2023  润新知