redis 锁

来源:互联网 发布:shopnc 源码下载 编辑:程序博客网 时间:2024/06/10 07:16

        项目中经常使用到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=0if (!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() {@Overridepublic void run() {String token = getLock(pool,key);if (token == null) {//没有获取到锁System.out.println("do not get lock");} else {//deal businessif (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() {@Overridepublic void run() {String token = getLock(pool,key);if (token == null) {//没有获取到锁System.out.println("do not get lock");} else {//deal businessif (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;@Componentpublic class RedisUtil {@Autowiredprivate 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);}}

单元测试

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


原创粉丝点击