java使用redis实现分布式锁

      通过上篇文章,已经知道分布式锁有哪些实现方案及其优缺点。本文记录下使用redis实现分布式锁的测试例子。

     使用Jedis的时候,建议使用版本2.6.0之上的,因为高版本set的时候,可以把key和过期时间一起原子性操作;2.6.0以下版本不行。网上有些文章的实现就是使用2.6.0之下的,这篇文章就分析了弊端。

   pom.xml



       redis.clients
	    jedis
	    3.0.1

RedisTool.java

  获取锁及释放锁。

package com.fei;

import java.util.Collections;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

public class RedisTool {

	private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
 
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
          //2.9.0版本
     //   String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        //3.0.1版本
    	String result = jedis.set(lockKey, requestId,SetParams.setParams().nx().px(expireTime));
    	
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
    
    private static final Long RELEASE_SUCCESS = 1L;
    
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
 
        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(lockKey), Collections.singletonList(requestId));
 
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
 
}

测试类

package com.fei;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;

import redis.clients.jedis.Jedis;
/**
 * 业务处理过程中耗时长,redis自动过期,其他线程也拿到锁,会导致数据错乱
 * @author Jfei
 *
 */
public class Test01 {

	
	private static String produceName = "iphone";//商品名称
	private static int produceNum = 20;//商品数量
	
	/**
	 * 不使用池,是为了模拟多个jvm产生的多个redis客户端
	 * 如果正式使用,最好设置JedisPool,避免每个JVM无限制产生多个redis客户端
	 * @return
	 */
	public static Jedis getJedis(){
		Jedis jedis = new Jedis("127.0.0.1",6379);
		jedis.auth("123456");
		return jedis;
	}
	
	public static void main(String[] args) {
		
		List threads = new ArrayList();
		for(int i=0 ;i<100;i++){
			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					String uuid = UUID.randomUUID().toString();
					Jedis jedis = Test01.getJedis();
					//只要还有商品,就一直抢锁;也可以设置尝试抢锁次数
					for(;;){
						if(RedisTool.tryGetDistributedLock(jedis, produceName, uuid, 1000	)){
							break;
						}
						//如果商品没了,那也不需要抢了
						 if(produceNum <=0){
						    	return;
						  }
						 
					}
				    //抢到锁了,但是商品没了
					 if(produceNum <=0){
					    	RedisTool.releaseDistributedLock(jedis, produceName, uuid);
					    	jedis.close();
					    	return;
					    }
					
				    int sleepMills = new Random().nextInt(800);//如果大于1000,那redis中过期了
				    int tmp = produceNum;
				    try {
				    	produceNum = produceNum -1;
				    	//模拟生成订单耗时,库存减少耗时,比如数据库中produce_num - 1
						Thread.sleep(sleepMills);//休眠
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				   
				    
					System.out.println(Thread.currentThread().getName() +" 休眠" + sleepMills + " mills " + " 抢到"+produceName + "  " + tmp);
					
					
					boolean releaseOk = RedisTool.releaseDistributedLock(jedis, produceName, uuid);
					System.out.println(Thread.currentThread().getName() + "释放锁" + (releaseOk?"成功":"失败"));
					jedis.close();
				}
					
		},"线程"+i);
			
		threads.add(t);
	}
		
		for(Thread t : threads){
			t.start();
		}
		
		for(Thread t : threads){
			try {
				t.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}
	
}

   假设有iphone手机20台,20个人抢(20个线程),redis中锁的过期时间是1000毫秒,加入业务处理总耗时不超过800毫秒。结果

线程27 休眠530 mills  抢到iphone  20
线程27释放锁成功
线程60 休眠443 mills  抢到iphone  19
线程60释放锁成功
线程99 休眠750 mills  抢到iphone  18
线程99释放锁成功
线程54 休眠63 mills  抢到iphone  17
线程54释放锁成功
线程11 休眠89 mills  抢到iphone  16
线程11释放锁成功
线程56 休眠153 mills  抢到iphone  15
线程56释放锁成功
线程4 休眠669 mills  抢到iphone  14
线程4释放锁成功
线程22 休眠95 mills  抢到iphone  13
线程22释放锁成功
线程25 休眠136 mills  抢到iphone  12
线程25释放锁成功
线程50 休眠515 mills  抢到iphone  11
线程50释放锁成功
线程18 休眠658 mills  抢到iphone  10
线程18释放锁成功
线程81 休眠206 mills  抢到iphone  9
线程81释放锁成功
线程70 休眠524 mills  抢到iphone  8
线程70释放锁成功
线程7 休眠185 mills  抢到iphone  7
线程7释放锁成功
线程66 休眠249 mills  抢到iphone  6
线程66释放锁成功
线程49 休眠11 mills  抢到iphone  5
线程49释放锁成功
线程72 休眠428 mills  抢到iphone  4
线程72释放锁成功
线程92 休眠390 mills  抢到iphone  3
线程92释放锁成功
线程12 休眠161 mills  抢到iphone  2
线程12释放锁成功
线程42 休眠799 mills  抢到iphone  1
线程42释放锁成功

  有序输出。但是如果业务总耗时超过1000毫秒(把代码int sleepMills = new Random().nextInt(800)中的800修改为2000),则表示redis中的锁过期了,被redis删除了,其他线程会抢到锁,进而购买商品,此时会有2个线程同时在消费商品。

线程16 休眠1495 mills  抢到iphone  20
线程16释放锁失败
线程18 休眠1336 mills  抢到iphone  19
线程18释放锁失败
线程89 休眠548 mills  抢到iphone  17
线程89释放锁成功
线程23 休眠1993 mills  抢到iphone  18
线程23释放锁失败
线程65 休眠1259 mills  抢到iphone  16
线程65释放锁失败
线程33 休眠1759 mills  抢到iphone  15
线程33释放锁失败
线程92 休眠1019 mills  抢到iphone  14
线程92释放锁失败
线程85 休眠374 mills  抢到iphone  13
线程85释放锁成功
线程90 休眠231 mills  抢到iphone  12
线程90释放锁成功
线程37 休眠778 mills  抢到iphone  11
线程37释放锁成功
线程71 休眠166 mills  抢到iphone  9
线程71释放锁成功
线程69 休眠1402 mills  抢到iphone  10
线程69释放锁失败
线程59 休眠1425 mills  抢到iphone  8
线程59释放锁失败
线程51 休眠808 mills  抢到iphone  7
线程51释放锁成功
线程80 休眠539 mills  抢到iphone  6
线程80释放锁成功
线程82 休眠227 mills  抢到iphone  4
线程82释放锁成功
线程72 休眠1705 mills  抢到iphone  5
线程72释放锁失败
线程54 休眠1581 mills  抢到iphone  3
线程54释放锁失败
线程84 休眠1053 mills  抢到iphone  2
线程84释放锁失败
线程43 休眠203 mills  抢到iphone  1
线程43释放锁成功

看到抢到的iphone不是有序的了,甚至可能重复会出现负数。

所以必须注意redis的过期时间。至于业务超时(也就是释放锁失败),可以根据实际情况处理,比如当做抢失败了,然后通过消息队列异步处理,删除订单,库存+1。

redis官方建议使用redisson,可以非常方便直接使用锁和释放锁。

你可能感兴趣的:(分布式)