redis 锁

        项目中经常使用到redis锁,锁最常用的场景在多线程操作共享资源时,需要对共享资源进行加锁,避免造成重复处理或处理时数据已经是脏数据.多个线程使用同一个锁,也就是锁必须独立与这些线程之外(也可以使用线程直接变量共享,用的少),在一个独立的应用中,锁可以直接存储在应用上,这样这个应用的其他线程都可以获取到,但是一旦牵扯到多个应         用以分布式的形式存在,这就需要分布式锁,而redis锁就是一种应用很广泛的分布式锁.

        达到分布式锁的目的有多种,DB锁就是其中一种,DB锁对一条记录加锁,多个连接竞争访问,而redis基于单进程单线程模式,采用队列模式将并发访问变成串行访问,多个客户端对redis的连接不存在竞争关系.

       redis实现业务锁的一种简单的方式:获取锁=key存在,加锁=设置key,释放锁=删除key

       redis加锁,获取锁都是采用set指令,set指令有set,setinx,setex,psetex,不过后三种都可以通过参数以set的方式实现(redis set指令)

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX :只在键已经存在时,才对键进行设置操作。
  redis加锁主要通过nx的形式,通过其返回,返回OK表示key不存在,设置key以及过期时间,表示获取到锁,而返回nil表示键已经存在了,不对key进行操作,表示获取锁失败.删除key即表示释放锁.jedis版本不同,实现方式也不同.redis2.6.12版本之前,redis不支持set(key,value,nxxx,pxxx,expire),而设值与设过期时间应该在同一个事务中:
package spring.redis.test;

import java.io.IOException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import org.springframework.util.CollectionUtils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Transaction;

public class TestRedis {
	public static boolean unLock(JedisPool pool,String key,String token) {
		Jedis jedis = pool.getResource();
		try {
			String tok = jedis.get(key);
			if (token.equals(tok)) {
				//释放锁
				Long del = jedis.del(key);
				if (del != null && del > 0) {
					return true;
				} else  {
					return false;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		} finally {
			if (jedis != null) {
				jedis.close();
				System.out.println(Thread.currentThread().getName()+"unlock close jedis");
			}
		}
		return false; 
	}
	 
	public static String  getLock(JedisPool pool,String key) {
		Jedis jedis = pool.getResource();
		 String token = UUID.randomUUID().toString();
		 String returnValue = null;
		 Transaction multi  = null;
		try {
			if (jedis.exists(key)) {
				System.out.println(Thread.currentThread().getName()+" lock held by other!");
			} else {//多线程时可能同时进入此
				jedis.watch(key);
				multi  =jedis.multi();
				multi.setnx(key, token);//result 只会返回0和1,0-已存在不做处理,1-不存在并设值
				multi.expire(key, 60*60);
				List<Object> exec = multi .exec();
				//jedis.unwatch();执行了exec就没必须要再执行了
				//exec执行成功exec={1,1},执行失败exec.size=0
				if (!CollectionUtils.isEmpty(exec)) {
					returnValue = token;
				}
			}
			 return returnValue;
		} catch (Exception e) {
			System.out.println(Thread.currentThread().getName()+" error to connect redis");
			e.printStackTrace();
			 return returnValue;
		} finally {
			if (jedis != null) {
				jedis.close();
				System.out.println(Thread.currentThread().getName()+"lock close jedis");
			}
			if (multi != null) {
				try {
					multi.clear();
					multi.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}  
	 } 
	
	 public static void main(String[] args) throws InterruptedException {
			JedisPoolConfig config = new JedisPoolConfig();
			config.setMaxIdle(20);
			config.setMaxTotal(40);
			config.setMinIdle(10);
			final JedisPool pool = new JedisPool(config, "localhost", 6379, 60 * 60);
			
			final String key="test";
			
			Thread[] threads = new Thread[2];
			for (int i = 0; i < 2; i++) {
				threads[i] = new Thread(new Runnable() {
					@Override
					public void run() {
						String token = getLock(pool,key);
						if (token == null) {//没有获取到锁
							System.out.println("do not get lock");
						} else {
							//deal business
							if (unLock(pool, key, token)) {//防止过期锁 删除key   
								System.out.println("success to unlock");
							} else {
								System.out.println("error to unlock");
							}
						}
					}
				});
			}
			
			for (Thread thread : threads) {
				thread.start();
			}
			
			TimeUnit.SECONDS.sleep(5);
			if (pool !=null) {
				pool.close();
				System.out.println("close pool");
			}
	}
}
       这里有个小疑惑,使用@Test注解测试 多线程总是报read Timeout,找不到原因(执行jedis.setnx())-有时间再回过头去看看.
redis2.6.12之后,就不需要事务了
package spring.redis.test;

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

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class TestRedis {
	public static boolean unLock(JedisPool pool,String key,String token) {
		Jedis jedis = pool.getResource();
		try {
			String tok = jedis.get(key);
			if (token.equals(tok)) {
				//释放锁
				Long del = jedis.del(key);
				if (del != null && del > 0) {
					return true;
				} else  {
					return false;
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		} finally {
			if (jedis != null) {
				jedis.close();
				System.out.println(Thread.currentThread().getName()+"close jedis");
			}
		}
		return false; 
	}
	 
	public static String  getLock(JedisPool pool,String key) {
		Jedis jedis = pool.getResource();
		 String token = UUID.randomUUID().toString();
		 String returnValue = null;
		try {
			String result = jedis.set(key, token,"NX","EX",60 *60);//返回 OK 或NULL
			 if ("OK".equalsIgnoreCase(result)) {//不存在,享有处理资源的权利,即获取到锁
				 System.out.println("success lock!");
				 returnValue =  token;
			 } else {//已存在()
				 System.out.println("error lock!");
			 }
			 return returnValue;
		} catch (Exception e) {
			System.out.println(Thread.currentThread().getName()+" error to connect redis");
			e.printStackTrace();
			return returnValue;
		} finally {
			if (jedis != null) {
				jedis.close();
			}
		}  
	 } 
	
	 public static void main(String[] args) throws InterruptedException {
			JedisPoolConfig config = new JedisPoolConfig();
			config.setMaxIdle(20);
			config.setMaxTotal(40);
			config.setMinIdle(10);
			final JedisPool pool = new JedisPool(config, "localhost", 6379, 60 * 60);
			
			final String key="test";
			
			Thread[] threads = new Thread[2];
			for (int i = 0; i < 2; i++) {
				threads[i] = new Thread(new Runnable() {
					@Override
					public void run() {
						String token = getLock(pool,key);
						if (token == null) {//没有获取到锁
							System.out.println("do not get lock");
						} else {
							//deal business
							if (unLock(pool, key, token)) {//防止过期锁 删除key   
								System.out.println("success to unlock");
							} else {
								System.out.println("error to unlock");
							}
						}
					}
				});
			}
			
			for (Thread thread : threads) {
				thread.start();
			}
			
			TimeUnit.SECONDS.sleep(5);
			if (pool !=null) {
				pool.close();
				System.out.println("close pool");
			}
	}
}

        此处将判断存在,同时设置时间放在一起了,也就不需要事务支持了

        再来看看用redisTemplate实现加锁,模板没有实现支持set(key,value,nxxx,pxxx,expire)(猜的),因此只能采用第一种方法.第一种方法钟使用token验证,采用token程序更健壮,防止过期锁删却还能删除.举一个应用场景:A获取taskLock,开始执行task,但锁的时间设置太短,任务没执行完,锁已经失效了,B又获取到了taskLock,但A却可以删除B刚刚获取到的锁.这样就失去了锁的意义了.项目中通常设置足够长的过期时间,一旦获取到任务锁,在过期时间内足够执行任务完毕,同时删除锁,这样就省去了进行token验证.代码如下:

package spring.redis.test.util;

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

@Component
public class RedisUtil {

	@Autowired
	private StringRedisTemplate redisTemplate;

	public boolean lock(String key, String value, int expire) {
		try {
			redisTemplate.watch(key);
			if (redisTemplate.hasKey(key)) {
				return false;
			}
			redisTemplate.multi();
			ValueOperations<String, String> vo = redisTemplate.opsForValue();
			if (expire != -1) {
				vo.set(key, value, expire, TimeUnit.SECONDS);
			} else {
				vo.set(key, value);
			}
			// ############################
			/*
			 * 此行用于判断是否执行成功,因为ValueOperations.set直接回调,exec()获取不到其执行结果
			 */
			redisTemplate.getExpire(key);
			// ############################
			List<Object> exec = redisTemplate.exec();
			if (CollectionUtils.isEmpty(exec)) {
				return false;
			}
			return true;
		} catch (Exception e) {
			return false;
		} finally {
			RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
		}
	}

	public void unlock(String key) {
		redisTemplate.delete(key);
	}
}

单元测试

	@Test
	public void set(){
		if (redisUtil.lock("test", "", 60)) {
			// deal business
			redisUtil.unlock("test");
			System.out.println("end");
		} else {
			System.out.println("fail to get lock!");
		}
	}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值