redis 分布式锁

使用redis,如果某个线程没有抢到锁,就让他睡眠一会儿。
误删:假设线程1获得了lock,过期时间是10s,如果第10秒redis执行了过期,删除了lock,而线程1下订单还需要两秒钟执行,那么第二个线程就会获得锁,第12s的时候,线程1删除锁lock,线程2失去锁lock,另外的线程3获取锁。也就是说,过期时间不应该小于业务执行时间,因此需要在下面的代码中引入守护进程
解锁时,需要看设置时的value与此时redis的value是否相同,如果不判断,有可能出现超卖,也就是说,上面的"第12s的时候,线程1删除锁lock,线程2失去锁lock,另外的线程3获取了锁" ,线程3获取了不应该得到的锁,有可能超卖(想想卖票的例子),如果不设置守护进程,线程1也不应该删去线程2的锁,因此解锁时需要额外判断
下面是加锁的过程,守护进程在其中实现:

@Component
public class RedisLock  {

    @Autowired
    private JedisPool jedisPool;
    private static final String key = "lock";
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private static AtomicBoolean isHappened = new AtomicBoolean(true);

    //加锁
    public void lock() {
        while (true) {
            boolean b = tryLock();  //尝试加锁
            if (b) {
                //拿到了锁
                return;
            }
            try {
               // 等待一段时间,之后再去获取锁,也可以设置跳出条件
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //尝试加锁,被lock调用
    public boolean tryLock() {
        SetParams setParams = new SetParams();
        setParams.ex(2);  //过期时间 2s
        setParams.nx();   // setNx的方式去设置,如果key已经存在,返回null
        String s = UUID.randomUUID().toString();
        Jedis resource = jedisPool.getResource();
        String lock = resource.set(key, s, setParams);
        resource.close();
        
        if ("OK".equals(lock)) {
            //拿到了锁,并且随即保存一个值
            threadLocal.set(s);
       // 设置守护进程,不断延长锁的过期时间,
            if (isHappened.get()) {
                ThreadUtil.newThread(new MyRUnble(jedisPool)).start();
                isHappened.set(false);
            }
            return true;
        }
        return false;
    }
    // 守护进程,保证在持有者执行任务的时候,redis中的值不会过期。
    static class MyRUnble implements Runnable {
        private JedisPool jedisPool;
        public MyRUnble(JedisPool jedisPool) {
            this.jedisPool = jedisPool;
        }
        @Override
        public void run() {
            Jedis jedis = jedisPool.getResource();
            while (true) {
                Long ttl = jedis.ttl(key);
                if (ttl != null && ttl > 0) {
                    jedis.expire(key, (int) (ttl + 1));
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

下面是解锁的代码:请放入RedisLock 中。


    //解锁,注意查看resource.eval方法参数的意思。

    public void unlock() throws Exception {
        String script = "if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" 
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
        Jedis resource = jedisPool.getResource();

        //正确的解锁方式
        Object eval = resource.eval(script, Arrays.asList(key), Arrays.asList(threadLocal.get()));
        if (Integer.valueOf(eval.toString()) == 0) {
            resource.close();
            throw new Exception("解锁失败");
        } 
        resource.close();
    }
}

配置类()

  @Configuration
public class SpringConfig {  @Bean
    public JedisPool jedisPool(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(101);
        jedisPoolConfig.setMaxIdle(1000);
        jedisPoolConfig.setMinIdle(1);
        jedisPoolConfig.setMaxWaitMillis(2000);
        jedisPoolConfig.setTestOnBorrow(true);
        jedisPoolConfig.setTestOnReturn(true);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,"xxx.xx.xx.xx",6379);
        return jedisPool;
    }
}

测试:countDownLatch用来控制主线程的结束,如果不设置,程序会很快结束。

    private int count = 6;
    @Autowired
    @Qualifier("redisLock")
    private RedisLock   lock;
    private CountDownLatch countDownLatch = new CountDownLatch(6);

  @Test
    public void Test() throws InterruptedException {
        TicketsRunBle ticketsRunBle = new TicketsRunBle();
        Thread thread1 = new Thread(ticketsRunBle, "窗口1");
        Thread thread2 = new Thread(ticketsRunBle, "窗口2");
        Thread thread3 = new Thread(ticketsRunBle, "窗口3");

        thread1.start();
        thread2.start();
        thread3.start();

        countDownLatch.await();
        Thread.currentThread().interrupt();
    }

    public class TicketsRunBle implements Runnable {
        @Override
        public void run() {
            while (count > 0) {
                lock.lock();
                try {
                    if (count > 0) {
                        Thread.sleep(ThreadLocalRandom.current().nextInt(100, 200));
                        countDownLatch.countDown();
                        System.out.println(Thread.currentThread().getName() + "售出第" + count-- + "张票");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        lock.unlock();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值