正确的实现redis分布式锁

目录

概述

什么是分布式,什么是分布式锁,为什么使用分布式锁

分布式锁应该具备哪些条件

分布式锁应用案例和效率分析

redis实现分布式原理

redis实现分布式锁方法:

第一种加锁(错误),使用setnx,和del(String key)。

第二种加锁(错误),使用setnx,和del和expire。

第三种加锁(错误),使用setnx,和del和getSet。

第四种加锁(错误),使用set,加Lua脚本

第五种加锁(正确),就是给第四种方法加守护线程。实现起来比较复杂,但已有人开发为工具就是Redisson:  Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】,

解锁一(错误)

解锁二,(错误)

解锁三,(正确)


概述

小编最近学习redis实现分布式,看了广大网友的博客,学习完后,发现有一半以上的博客redis分布式锁是有错误所以写此文章让大家学习正确的redis分布式锁。本文会讲用setnx,和set等五种加锁方法和三种解锁方法,并指出其错误点。

什么是分布式,什么是分布式锁,为什么使用分布式锁

1.什么是分布式?来一张图

当大量用户请求服务器时,如果一个服务器去相应的话,那么这台服务器的压力会特别大,有可能会崩溃,最好的方式是分发给此刻压力较小的服务器去处理。这个服务器可以是单独一台电脑,也可以同一台电脑中的其他进程。但是他们的读数据都是从数据库中读。

2.什么是分布式锁,为什么使用分布式锁,如下场景

假如双11有一个秒杀活动,秒杀100件短袖,同一秒有上万人秒杀,中转服务器将上万个请求散发给处理服务器们,他们处理时最重要的业务(比如删除修改)同一时刻只能一个服务器处理,好比如单机下给一个方法加synchronized,否则就会出现多卖的情况。这时候就要给最重要的业务或方法加分布式锁。保证不会出现多卖情况

分布式锁应该具备哪些条件

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
  • 具备锁失效机制,防止死锁
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁应用案例和效率分析

场景:假如双11有一个秒杀活动,秒杀100件短袖,同一秒有上万人秒杀,

方法:由于是单机情况,所以我用10000线程(模拟进程)同一时刻去提交抢购短袖。

package com.util.test;

public class TenProgress implements Runnable{
	Seriver seriver;
	
	
	public TenProgress(Seriver seriver) {
		this.seriver = seriver;
	}
	
	public void start(){
		new Thread(this,"秒杀线程").start();
	}

	@Override
	public void run() {
		for(int i = 0; i < 10000; i++){
			new Thread(new seckill("短袖"), "秒杀线程" + i).start();
		}
	}
	
	class seckill implements Runnable{
		String killName;
		
		public seckill(String Killname) {
			this.killName = Killname;
		}
		
		@Override
		public void run() {
			seriver.order1(killName);
		}
	}
}
public class Seriver {
    //商品总数
    private static HashMap<String, Integer> product = new HashMap<>();
    //订单表
    private static HashMap<String, String> orders = new HashMap<>();
    //库存表
    private static HashMap<String, Integer> stock = new HashMap<>();
    
    static {
        product.put("短袖", 100);
        stock.put("短袖", 100);
    }
    //开启抢购线程
    public void startKill() {
    	TenProgress progress = new TenProgress(this);
    	progress.start();	
    }

    public void select_info(String product_id,String id) {
        System.out.println("限量抢购商品XXX共" + product.get(product_id) + ",现在成功下单" + orders.size()
        + ",剩余库存" + stock.get(product_id) + "件,当前" + "订单号:" + id);
    }
    
    //加reids分布式锁
    public void order1(String product_id) {
    	String value = System.currentTimeMillis() + 2000 + "";
    	
    	if(redis.lock4(product_id, value)){
    		if (stock.get(product_id) == 0) {
    			System.out.println(Thread.currentThread().getName() + "没有抢到");
            } else {
            	String str = UUID.randomUUID().toString();
                orders.put(UUID.randomUUID().toString(), product_id);
                stock.put(product_id, stock.get(product_id) - 1);
                select_info(product_id,str);
            }
			redis.unlock3(product_id, value);
    	} else{
    		System.out.println(Thread.currentThread().getName() + "没有抢到");
    	}
    }
    
    //加synchronized锁
    public synchronized void order2(String product_id) {
        if (stock.get(product_id) <= 0) {
        	System.out.println(Thread.currentThread().getName() + "没有抢到");
            //已近买完了
        } else {
            //还没有卖完
            try {
                //模拟操作数据库
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String id= UUID.randomUUID().toString();
            orders.put(id, product_id);
            stock.put(product_id, stock.get(product_id) - 1);
            select_info(product_id,id);
        }  
    }
    
    //不加锁
    public void order3(String product_id) {
        if (stock.get(product_id) <= 0) {
        	System.out.println(Thread.currentThread().getName() + "没有抢到");
            //已近买完了
        } else {
            //还没有卖完
            try {
                //模拟操作数据库
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String id= UUID.randomUUID().toString();
            orders.put(id, product_id);
            stock.put(product_id, stock.get(product_id) - 1);
            select_info(product_id,id);
        }
    }
	
}

不加锁的情况下,运行情况如下

 程序执行时间大概是3s左右,可以看到首先是多卖了,其次库存有100件变为3674件,明显出现混乱,

加synchronized测试如下:

程序完美运行,但是程序执行时间为13s,秒杀活动是秒杀,耗时13秒明显不合理,况且并发量不到1000,如果上万那么就会更慢

加redis分布式锁,测试如下:

程序完美运行,程序执行时间为2s左右,效率最高

redis实现分布式原理

1.相关配置,重点不在配置,这里为了让大家看清楚,我就简单的实现其配置。太多反而不利于阅读

public Redis() {
            //配置连接池
	    JedisPoolConfig config = new JedisPoolConfig(); 
            //设置最大连接数
	    config.setMaxTotal(200);
            //设置最小空闲数
	    config.setMaxIdle(8); 
            //设置最大等待时间
	    config.setMaxWaitMillis(1000 * 1000); 
	    config.setTestOnBorrow(true); 
	    
	    jedispool = new JedisPool(config,"127.0.0.1",6379);
}

2.必须要了解的方法,尤其是set方法

(1)jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:第一个第二个就不用说了,第三个是填写方式,一个有两个方式,1."nx" 2."xx"。填写"nx",意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作,xx意思SET IF  EXIST;第四个参数是时间单位格式,有两种1."ex"  "px",ex是秒,px是毫秒,最后一个是过期时间值,如果写1,和第四参数ex组合就是1s后失效。和px组合就是1毫秒后失效

(2)setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回1,否则返回0。

(3)get(key):获得key对应的value值,若不存在则返回null。

(4)getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为新的value。

(5)expire(key, seconds):设置key-value的有效期为seconds秒

(6)del(String key):删除对应的键

3.用redis实现分布式锁原因

(1)在分布式系统下,使用redis集群,所有进程可以操作一个redis缓存,并且redis读写效率远高于sql数据库

(2)redis是单线程工作模式,也就是同一时刻,一个方法只能有一个人执行,且运行速度快

(3)redis可以设置失效时间,以及续加时间。

redis实现分布式锁方法:

第一种加锁(错误),使用setnx,和del(String key)。

        //第一种加锁,
	//方案:加锁直接返回true,没有设置失效时间,客户端执行完任务后自己释放
	//错误说明:如果某个客户端在获取锁后程序崩溃,或断电则锁永远不会被释放
	public boolean lock1(String key, String value) {
            Jedis jedis = jedispool.getResource();
	    //不等于0,说明设置成功,说明锁没有被占领,返回true,代表获取锁
	     if (jedis.setnx(key, value) != 0) {
	    	 jedis.close();
	         return true;
	     }
	     jedis.close();
	     return false;
	}

错误说明:如果某个客户端在获取锁后程序崩溃,或断电则锁永远不会被释放

第二种加锁(错误),使用setnx,和del和expire。

        //第二种加锁
	//方案:加锁,设置失效时间,时间到了自动删除键。
	//错误说明:还是会出现锁永远不会被释放的情况,如下
	public boolean lock2(String key, String value) {
		Jedis jedis = jedispool.getResource();
		//不等于0,说明设置成功,说明锁没有被占领,返回true,代表获取锁
	    if (jedis.setnx(key, value) != 0) {
	    	//如果客户端在此处崩溃或者断电,则永远不会释放锁
	    	jedis.expire(key, 5);
	    	jedis.close();
	        return true;
	    }
	    jedis.close();
	    return false;
	}

错误说明:锁有可能永远不会被释放

第三种加锁(错误),使用setnx,和del和getSet。

        //第三种加锁,
	//方案:加锁,不设置失效时间,采用value做失效时间,value放的值为"当前时间"+"有效时间",
	//若客户端崩溃,但该key(锁)已经被赋值,判断value的值是否小于当前时间,小于则代表失效,可以获取其锁。
	//错误点:看完代码待会再说
	public boolean lock3(String key,String value) {
		Jedis jedis = jedispool.getResource();
		//假如有效时间为5秒
		long expiretime = System.currentTimeMillis() + 5000;
		String expiretimeStr = String.valueOf(expiretime);
		//不等于0,说明设置成功,说明锁没有被占领,返回true,代表获取锁
	    if (jedis.setnx(key, expiretimeStr) != 0) {
	    	jedis.close();
	        return true;
	    }
	     
	    //说明没有获取到锁,锁被其他客户端占领,
	    //也有可能锁过了失效时间,所以要判断,当前value的值对应的时间是否小于当前时间,小于,则说明锁失效
	    String keyExpireTime = jedis.get(key);
	    if(keyExpireTime != null && Long.parseLong(keyExpireTime) < System.currentTimeMillis()){
	    	//进入这个方法你可能觉得设置value失效时间返回true就结束了,你会如下这样做
	    	//jedis.set(key, value)
	    	//return true;
	    	//事实并不是如此,因为你忘了高并发情况下可能多个客户端同时进入到这个条件里,就会多个同时获取锁,正确如下
	    	//首先设置锁的失效时间并获取其旧值(旧的失效时间),考虑到高并发只能用getSet这一条命令,不能分开写
	    	String oldValueStr = jedis.getSet(key, expiretimeStr);
	    	//其次oldValueStr有可能为null,如果为null,equals会报错,所以用&&(短路运算),在高并发情况下虽然getSet可能被多个客户端执行,
	    	//但返回值oldValueStr只有一个和keyExpireTime相等,相等的那个获取锁,也就是有且只有一个能得到锁
	    	if(oldValueStr != null && oldValueStr.equals(keyExpireTime)){
	    		jedis.close();
	    		return true;
	    		//你可能觉得想不到哪里有错,其实只是不完美,有瑕疵。如下
	    		//1.jedis.getSet(key, expiretimeStr);这条语句可能被多次执行,虽然获取了锁,但是锁的失效时间可能被其他客户端覆盖
	    		//2.锁不具有拥有者标识,解锁直接根据key进行解锁,即任何客户端都可以解锁,不安全。
	    		//3.需要分布式下每个客户端的时间必须同步。
	    	}
	    	
	    }
	    jedis.close();
	    return false;
	}

错误如下:1需要分布式下每个客户端的时间必须同步
                2.锁不具有拥有者标识,解锁直接根据key进行解锁,即任何客户端都可以解锁,不安全。
                3.jedis.getSet(key, expiretimeStr);这条语句可能被多次执行,虽然获取了锁,但是锁的失效时间可能被其他客户端覆盖

第四种加锁(错误),使用set,加Lua脚本

        //第四种加锁,
	//方案:使用set(String key, String value, String nxxx, String expx, int time)
	//同时设置了值和失效时间。因为set方法是原子性的同一时刻只能有一个执行
	//错误点:其lock4是目前网上主流用redis实现分布式锁,但是小编发现它还是有问题,先看代码
	public boolean lock4(String key, String value) {
		 Jedis jedis = jedispool.getResource();
		 String res = jedis.set(key, value, "nx", "ex", 5);
	     if (res != null && res.equals("OK")) {
	    	 jedis.close();
	         return true;
	     }
	     jedis.close();
	     return false;
	}
	//错误点:当客户端A拿到了锁,但是客户端A因为系统卡顿,或业务多原因处理该业务需要8秒时间,
	//假如锁的失效时间设置为5s,5秒后就会自动释放,此时客户端B就可以获取锁,也就是该业务同时两个人在做
	//这明显会造成不安全行为。
	//解决方法为:A客户端自己为自己,开启一个守护线程,当获取锁时代码未执行完,续加失效时间。
	//那么客户端宕机了怎么办?啥都不做,它宕机了,他就不能为自己续加时间。时间到锁就过期了,完美解决这一问题。

错误点:当客户端A拿到了锁,但是客户端A因为系统卡顿,或业务多原因处理该业务需要8秒时间,假如锁的失效时间设置为5s,5秒后就会自动释放,此时客户端B就可以获取锁,也就是该业务同时两个人在做,这明显会造成不安全行为。

因为网上大多数博客都是这种方法所有小编图解下错误,帮助大家理解

这是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是 5 秒。

如果某些原因导致线程 A 执行的很慢很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。

随后,线程 A 接着执行任务,也就是同一时间有 A,B 两个线程在访问代码块,如果此时线程B已经抢光短袖了,但线程A已经下单了,却没有货了。这明显是不合理的情况。

解决方法为:A客户端自己为自己,开启一个守护线程,当获取锁时代码未执行完,续加失效时间。那么客户端宕机了怎么办?啥都不做,它宕机了,他就不能为自己续加时间。时间到锁就过期了,完美解决这一问题。

 当线程 A 执行完任务,会显式关掉守护线程

第五种加锁(正确),就是给第四种方法加守护线程。实现起来比较复杂,但已有人开发为工具就是Redisson:  Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。【Redis官方推荐】,

加锁如下:

RLock mylock = redisson.getLock(key);
设置时间
mylock.lock(2, TimeUnit.MINUTES);

解锁如下:

RLock mylock = redisson.getLock(key);
      //释放锁(解锁)
mylock.unlock();

解锁一(错误)

        //第一种解锁,适用于第三种加锁
	//方案:根据key直接删除,
	//错误点:不安全任何客户端都可以解锁,
	public void unlock1(String key) {
		Jedis jedis = jedispool.getResource();
                jedispool.getResource().del(key);
                jedis.close();
	}

解锁二,(错误)

        //第二种解锁,适用于第一,第二,第四种加锁
	//方案:根据key,和value,当jedis.get(key)和value一样的时候在删除,
	//错误点:注意还是存在线程安全,
	//1.因为判断和删除是两行代码,当进入到{}里时,正好锁(key)过期,其他线程拿到锁,而此时又把锁删除了。这是不安全的
	//2.因为if进行判断时锁正好过期,而其他线程还没有拿锁,此时不存在key,jedis.get(key)的返回值为null,null不能进行equals
	public boolean unlock2(String key, String value) {
		Jedis jedis = jedispool.getResource();
        if (value != null && jedis.get(key).equals(value)) {
        	jedispool.getResource().del(key);
        	jedis.close();
        	return true;
        }
        jedis.close();
        return false;
	}

解锁三,(正确)

        //第三种解锁,适用于第一,第二,第四种加锁
	//方案:使用Lua脚本将判断,获取,删除,一次执行。
	public boolean unlock3(String key, String value) {
		Jedis jedis = jedispool.getResource();
		String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
		
		Object result = jedis.eval(script,Collections.singletonList(key),Collections.singletonList(value));
		if("OK".equals(result)){
			jedis.close();
			return true;
		}
        jedis.close();
        return false;
	}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值