Redis实现锁

Redis实现锁

摘要

Redis实现锁

Redis实现锁

参考 http://redisdoc.com/string/set.html
String set的时候可以添加NX选项

# 使用 NX 选项

redis 127.0.0.1:6379> SET not-exists-key "value" NX
OK      # 键不存在,设置成功

redis 127.0.0.1:6379> GET not-exists-key
"value"

redis 127.0.0.1:6379> SET not-exists-key "new-value" NX
(nil)   # 键已经存在,设置失败

redis 127.0.0.1:6379> GET not-exists-key
"value" # 维持原值不变

依靠NX的原理,如果已经存在就不进行自己的相关逻辑.

代码

RedisLock.java

package net.javablog.ut;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import net.javablog.ut.JedisUtil;

import java.util.UUID;

public class RedisLock {

    final static String UNLOCK_SCRIPT = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + 
            "    return redis.call(\"del\",KEYS[1])\n" + 
            "else\n" + 
            "    return 0\n" + 
            "end";

    public static Logger log = LoggerFactory.getLogger(RedisLock.class);
    public static boolean lock(String key, String id) {
        return lock(key,id,3600*2*1000);
    }


    public static boolean lock(String key, String id, long timeoutMills) {
        Jedis jedis = null ;
        try {
            jedis = JedisUtil.getInstance().getJedis();
            String s = jedis.set(key, id, "NX", "PX", timeoutMills);
            log.debug("set {} {} NX PX {}",key,id,timeoutMills);
            return "OK".equals(s);
        }finally {
            JedisUtil.getInstance().closeJedis(jedis);
        }
    }
    public static boolean unlock(String key, String id) {
        Jedis jedis = null ;
        try {
            jedis = JedisUtil.getInstance().getJedis();
            Object o = jedis.eval(UNLOCK_SCRIPT, 1, key,id);
            log.debug("unlock result:{}",o);
            return true;
        }finally {
            JedisUtil.getInstance().closeJedis(jedis);
        }
    }

    public static void lockAndGo(String key, long timeoutMills, Runnable run) {
        String id = UUID.randomUUID().toString();
        try {
            if(lock(key, id, timeoutMills)) {
                log.debug("locked : {}",  key);
                run.run();
            }else {
                log.debug("fail to lock : {}" , key);
            }

        }finally {
            unlock(key, id);
            log.debug("unlocked : {}" ,key);
        }
    }
}

JedisUtil.java

package net.javablog.ut;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@SuppressWarnings("all")
public class JedisUtil {
    protected Logger log = LoggerFactory.getLogger(getClass());

    private static int port = 6379;
    private static String ip = "127.0.0.1";

    /**
     * 私有构造器.
     */
    private JedisUtil() {
        if (ip.length() == 0)
            throw new RuntimeException("redis host url is not initialized.");
    }

    private static Map<String, JedisPool> maps = new ConcurrentHashMap<String, JedisPool>();

    /**
     * 获取连接池.
     *
     * @return 连接池实例
     */
    private static JedisPool getPool() {
        String key = ip + ":" + port;
        JedisPool pool = null;
        //这里为了提供大多数情况下线程池Map里面已经有对应ip的线程池直接返回,提高效率
        if (maps.containsKey(key)) {
            pool = maps.get(key);
            return pool;
        }

        //这里的同步代码块防止多个线程同时产生多个相同的ip线程池
        synchronized (JedisUtil.class) {
            if (!maps.containsKey(key)) {
                JedisPoolConfig config = new JedisPoolConfig();
                config.setMaxTotal(200);
                config.setMaxIdle(10);
                config.setMaxWaitMillis(3000);
                config.setTestOnBorrow(true);
                config.setTestOnReturn(true);
                try {
                    /**
                     * 如果你遇到 java.net.SocketTimeoutException: Read timed out
                     * exception的异常信息 请尝试在构造JedisPool的时候设置自己的超时值.
                     * JedisPool默认的超时时间是2秒(单位毫秒)
                     */
                    pool = new JedisPool(config, ip, port);
                    maps.put(key, pool);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                pool = maps.get(key);
            }
        }
        return pool;
    }

    /**
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
     */
    private static class RedisUtilHolder {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static JedisUtil instance = new JedisUtil();
    }

    /**
     * 当getInstance方法第一次被调用的时候,它第一次读取
     * RedisUtilHolder.instance,导致RedisUtilHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静
     * 态域,从而创建RedisUtil的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
     * 这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
     */
    public static JedisUtil getInstance() {
        return RedisUtilHolder.instance;
    }

    /**
     * 获取Redis实例.
     *
     * @return Redis工具类实例
     */
    public Jedis getJedis() {
        Jedis jedis = null;
        int count = 0;
        do {
            try {
                jedis = getPool().getResource();
                // log.info("get redis master1!");
            } catch (Exception e) {
                log.error("get redis master1 failed!", e);
                // 销毁对象
//                getPool().returnBrokenResource(jedis);
                if (jedis != null) {
                    jedis.close();
                }
            }
            count++;
        } while (jedis == null && count < 5);
        return jedis;
    }

    /**
     * 释放redis实例到连接池.
     *
     * @param jedis redis实例
     */
    public void closeJedis(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

    public static void setHost(String redisHost) {
        ip = redisHost;
    }
}

如何使用?

保证相同的msgid只能被一次逻辑处理

final String msgid_f = "XXX";
Runnable r = new Runnable() {
    @Override
    public void run() {
        log.info("save msgid {}", msgid_f);
    }
};
String key = "LOCK_saveMsgidTag_" + msgid_f;
RedisLock_msg.lockAndGo(key, 3600 * 2 * 1000, r);