Redis分布式锁实现及相关注重问题

0.前言

      在多线程并发的情况下,我们可以使用锁来保证代码在同一时间只能一个线程访问,比如synchronize或者lock。但在分布式的集群环境,就需要使用分布式锁。
     分布式:一个业务拆分为多个子业务,部署在多个服务器上 。
     集群:同一个业务,部署在多个服务器上 。

1.场景:

     a).避免不同节点(业务)重复相同工作,
     b).避免破坏数据的正确性
           比如,同一服务部署在不同的服务器上,同时接受到请求,同时对数据进行修改(如i- -),则可能出现重复- -的情况。
Redis分布式锁实现及相关注重问题_第1张图片

2.Redis分布式锁原理:

     Set命令附加NX和PX属性
在这里插入图片描述
     主要依托了redis的 Key 不存在时才能 Set 成功的特性
     A服务set成功(获取锁成功),那么B服务就无法获取改Key的锁,除非A服务释放了锁,B服务才能获取到。,

3.Redis分布式锁主要关注的问题:

     a.锁超时:
          A服务set成功(获取了锁),之后A服务突然挂掉了,那么其它服务就无法获取到锁了,为了避免这种问题,所以需要添加锁过期时间,像上述图片t2在30s后会自动失效,即释放锁。
     b.原子性问题:
          加锁的时候,如果先set t2 666,然后再expire t2 30 ;这样的操作不是原子性的,也就是说,如果set t2 666后突然服务挂了,又还没来得及设置过期时间,则其他服务永远也获取不到锁,但是set t2 666 PX 30000 NX这样就是原子性的。
          解锁的时候,如果直接jedis.del(key);可能会删除不是自己加的锁。,
          是否会想到下述的操作,先get判断再del,但这并不是原子性的操作,比如A服务加锁后,到了过期时间自动释放了锁,之后B服务获取了锁,此时A服务业务逻辑执行完后,直接就把锁给删了,服务就乱套了。

if (value.equals(jedis.get(key))) {
     
    // 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁
    jedis.del(key);
}

          正确释放锁,需要执行lua脚本来进行。
Redis分布式锁实现及相关注重问题_第2张图片
     c.业务执行超过自动过期时间:
          A服务获得了锁,业务执行时间超过了过期时间,A服务还未手动释放锁就自动释放了锁,然后B服务获得了锁,也有可能出现别的问题,所以为了避免这个问题, Redis 分布式锁不要用于较长时间的任务。
          或者对获得锁的线程启动一个守护线程,用来给锁续期(执行expire命令),执行时间就变长了这样。
     d.请求是否不可丢失问题:
          如果请求不允许丢失,即每个服务都要获取锁来执行业务逻辑,则不断重试获取锁,直到获取锁成功。
          如果允许丢失,则尝试重试到设置的重试时间阈值,无法获取锁则不管了,也就不执行业务逻辑了。

4.Redis分布式锁代码实现:

     加锁与释放锁的类:

package com.zwh.JedisDistributed;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;


public class RedisLock {
     
    public String LOCK_KEY = "reids_lock";
    private long AUTO_EXPIRE_TIME;          //锁自动释放阈值
    private long reTryTime;                 //重试时间阈值
    private SetParams params ;
    private static GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    public static JedisPool jedisPool = null;

    public RedisLock(Integer inventory, long AUTO_EXPIRE_TIME,long reTryTime) {
     
        this.AUTO_EXPIRE_TIME = AUTO_EXPIRE_TIME;
        this.params = SetParams.setParams().nx().px(AUTO_EXPIRE_TIME);
        this.reTryTime=reTryTime;
        poolConfig.setMaxIdle(inventory);
        poolConfig.setMaxTotal(inventory);
        poolConfig.setMaxWaitMillis(2000);//2s后报错
        jedisPool = new JedisPool(poolConfig, "192.168.199.188", 6379,4000);//4s后报连接超时
    }

    /**
     *  尝试获取锁,到了重试时间阈值退出,即可能请求丢失
     * */
    public String lock(String id) {
     
        Long start = System.currentTimeMillis();
        Jedis jedis = null;
        try {
     
            jedis = jedisPool.getResource();
            for (; ; ) {
     
                //SET命令返回OK ,则证明获取锁成功
                String lock = jedis.set(LOCK_KEY, id, params);
                if ("OK".equals(lock)) {
     
                    return "OK";
                }
                //否则循环等待,在timeout时间内仍未获取到锁,则获取失败
                long l = System.currentTimeMillis() - start;
                if (l >= reTryTime) {
     
                    return "TimeOut";
                }
                Thread.sleep(200);
            }
        } catch (Exception e) {
     
            System.out.println("******异常*******:"+e.toString());
        } finally {
     
            try {
     
                if (jedis != null) {
     
//                    System.out.println(jedisPool.getNumWaiters()+"----链接活跃数:"+jedisPool.getNumActive()+"----空闲连接数:"+jedisPool.getNumIdle());
                    jedis.close();
//                    System.out.println(jedisPool.getNumWaiters()+"----Close后活跃数:"+jedisPool.getNumActive()+"----空闲连接数:"+jedisPool.getNumIdle());
                }
            } catch (Exception e) {
     
                e.printStackTrace();
            }
        }
        return "";
    }
/**
 * 释放锁方法
 * */
    public boolean unlock(String id) {
     
        Jedis jedis = null;
        String script =
                "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                        "   return redis.call('del',KEYS[1]) " +
                        "else" +
                        "   return 0 " +
                        "end";
        try {
     
            jedis = jedisPool.getResource();
            String result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();
            return "1".equals(result) ? true : false;
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            try {
     
                jedis.close();
            } catch (Exception e) {
     
                e.printStackTrace();
            }
        }
        return false;
    }
}

     测试类:

import com.zwh.JedisDistributed.RedisLock;
import redis.clients.jedis.Jedis;

import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test {
     
	private static Integer POOL_SIZE=1001;
	private static long AUTO_EXPIRE_TIME = 3000;
	private static int NUM =1000;
    private static long reTryTime = 50000;
	private static LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue();
	static RedisLock redisLock = new RedisLock(POOL_SIZE,AUTO_EXPIRE_TIME,reTryTime);

    public static void getDeamon() {
     
        Thread deamonThread = new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                try (Jedis jedis = redisLock.jedisPool.getResource()) {
     
                    while (true){
     
                        Thread.sleep(2000);
                        System.out.println("ttl:"+jedis.ttl(redisLock.LOCK_KEY));
                        jedis.expire(redisLock.LOCK_KEY, 3);
//                        System.out.println("----------续期了----------"+Thread.currentThread().getName());
                    }
                } catch (Exception e) {
     
                    System.out.println("守护线程异常:" + e.toString());
                }
            }
        });
        deamonThread.setDaemon(true);
        deamonThread.start();
    }

    /**
     *  业务处理超过自动过期时间,守护线程为获取锁续期。
     * */
    public static void main(String[] args) {
     
        ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
        for (int i = 0; i <50; i++) {
     
            final int finalI = i;
            executor.execute(new Runnable() {
     
                @Override
                public void run() {
     
                    String uuid =UUID.randomUUID().toString();
                    String lockResult = redisLock.lock(uuid);   //重试获取锁失败时,请求丢失
                    if ("OK".equals(lockResult)){
     
                        getDeamon();                            //启动守护线程,每2s续期3s
                        try {
     
                            Thread.sleep(2000);  //业务逻辑处理耗时
                            NUM--;
                            System.out.println("-------库存NUM:"+NUM);
                        } catch (InterruptedException e) {
     
                            e.printStackTrace();
                        }
                        boolean unlock = redisLock.unlock(uuid);
//                        System.out.println("************unlock:"+unlock+"******"+Thread.currentThread().getName());
                    }else {
     
                        System.out.println("重试超时:"+lockResult);
                    }
                }
            });
        }
        executor.shutdown();
    }
/**
 *  业务处理超过自动过期时间,不做处理
 * */
    public static void main1(String[] args) {
     
        ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
        for (int i = 0; i <100; i++) {
     
//            final int finalI = i;
            executor.execute(new Runnable() {
     
                @Override
                public void run() {
     
                    String uuid =UUID.randomUUID().toString();
                    String lockResult = redisLock.lock(uuid);//重试获取锁失败时,请求丢失
                    if ("OK".equals(lockResult)){
     
                        long start = System.currentTimeMillis();
                        try {
     
                            Thread.sleep(100);  //业务逻辑处理耗时
                            NUM--;
                            System.out.println("-------库存NUM:"+NUM);

                        } catch (InterruptedException e) {
     
                            e.printStackTrace();
                        }
                        long end = System.currentTimeMillis();

                        if ((end-start)<=AUTO_EXPIRE_TIME){
     
                            boolean unlock = redisLock.unlock(uuid);    //成功获得了锁并且业务处理为超时
                        }else{
     
                            System.out.println("------------------业务超时---------------------");
                        }
                    }else {
     
                        System.out.println("重试超时:"+lockResult);
                    }
                }
            });
        }
        executor.shutdown();
    }
}

参考资料:
     漫画:什么是分布式锁?
     Redis—分布式锁深入探究
     姗姗来迟的Redis分布式锁
代码地址:
     https://github.com/OooooOz/Redis
//-------------------------------------------------------相互交流--------------------------------------------------//

你可能感兴趣的:(Redis,redis,分布式,多线程,jedis,java)