Redis分布式锁

前言

1、Jedis分布式锁

1.1、锁的工具类

package com.hlj.redis.lock.utils;
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
 * @Desc:
 * @Author HealerJean
 * @Date 2018/9/13  上午11:31.
 */
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";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     * 第一个为key,我们使用key来当锁,因为key是唯一的。
        第二个为value,我们传的是requestId,
                    很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
        第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
        第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
        第五个为time,与第四个参数相呼应,代表key的过期时间。
         */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 释放分布式锁
     *
     * @param jedis     Redis客户端
     * @param lockKey   锁
     * @param requestId 请求标识
     * @return 是否释放成功
     * 那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
//        第一行代码,我们写了一个简单的Lua脚本代码
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//        参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

1.2、进行测试

package com.hlj.redis.lock.service;

import com.hlj.redis.lock.utils.RedisTool;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @Desc:
 * @Author HealerJean
 * @Date 2018/9/13  上午11:32.
 */
public class RedisLockConsumerService {

    //库存个数
    static int goodsCount = 10;

    //卖出个数
    static int saleCount = 0;

    public static void main(String[] args) {
        JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379, 1000);

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {Thread.sleep(2);} catch (InterruptedException e) {}
                Jedis jedis = jedisPool.getResource();
                boolean lock = false;
                while (!lock) {
                    lock = RedisTool.tryGetDistributedLock(jedis, "goodsCount", Thread.currentThread().getName(), 10);
                }
                if (lock) {
                    if (goodsCount > 0) {
                        goodsCount--;
                        System.out.println("剩余库存:" + goodsCount + " 卖出个数" + ++saleCount);
                    }
                }
                RedisTool.releaseDistributedLock(jedis, "goodsCount", Thread.currentThread().getName());
                jedis.close();
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        jedisPool.close();
    }
}

2、redisTemplate锁

2.1、定义锁的接口

package com.hlj.redis.lock.utils;

/**
 * redis锁,原地址https://gitee.com/itopener/springboot/tree/master
 */
public interface DistributedLock {


    public static final long TIMEOUT_MILLIS = 30000; //超时时间

    public static final int RETRY_TIMES = Integer.MAX_VALUE; //重试次数

    public static final long SLEEP_MILLIS = 500; //重试时 线程休眠次数

    public boolean lock(String key);

    public boolean lock(String key, int retryTimes);

    public boolean lock(String key, int retryTimes, long sleepMillis);

    public boolean lock(String key, long expire);

    public boolean lock(String key, long expire, int retryTimes);

    public boolean lock(String key, long expire, int retryTimes, long sleepMillis);

    public boolean releaseLock(String key);
}



2、2、实现上述接口的抽象类

package com.hlj.redis.lock.utils;

/**
 * redis锁,原地址https://gitee.com/itopener/springboot/tree/master
 */
public abstract class AbstractDistributedLock implements DistributedLock {


    @Override
    public boolean lock(String key) {
        return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
    }

    @Override
    public boolean lock(String key, int retryTimes) {
        return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
    }

    @Override
    public boolean lock(String key, int retryTimes, long sleepMillis) {
        return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
    }

    @Override
    public boolean lock(String key, long expire) {
        return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
    }

    @Override
    public boolean lock(String key, long expire, int retryTimes) {
        return lock(key, expire, retryTimes, SLEEP_MILLIS);
    }

}

2.3、具体服务层实现分布式锁

package com.hlj.redis.lock.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;

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

/**
 * redis锁,原地址https://gitee.com/itopener/springboot/tree/master
 */
@Service("redisDistributedLock")
public class RedisDistributedLock extends AbstractDistributedLock {

    private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);

    private RedisTemplate<Object, Object> redisTemplate;

    private ThreadLocal<String> lockFlag = new ThreadLocal<String>();

    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) {
        super();
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, expire);
        // 如果获取锁失败,按照传入的重试次数进行重试
        while ((!result) && retryTimes-- > 0) {
            try {
                logger.debug("lock failed, retrying..." + retryTimes);
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                return false;
            }
            result = setRedis(key, expire);
        }
        return result;
    }

    private boolean setRedis(String key, long expire) {
        try {
            String result = redisTemplate.execute(new RedisCallback<String>() {
                @Override
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                    String uuid = UUID.randomUUID().toString();
                    lockFlag.set(uuid);
                    //PX millionSecond
                    return commands.set(key, uuid, "NX", "PX", expire);
                }
            });
            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
        }
        return false;
    }

    @Override
    public boolean releaseLock(String key) {
        // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
        try {
            List<String> keys = new ArrayList<String>();
            keys.add(key);
            List<String> args = new ArrayList<String>();
            args.add(lockFlag.get());

            // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本

            Long result = redisTemplate.execute(new RedisCallback<Long>() {
                public Long doInRedis(RedisConnection connection) throws DataAccessException {
                    Object nativeConnection = connection.getNativeConnection();
                    // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                    // 集群模式
                    if (nativeConnection instanceof JedisCluster) {
                        return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
                    }

                    // 单机模式
                    else if (nativeConnection instanceof Jedis) {
                        return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
                    }
                    return 0L;
                }
            });

            return result != null && result > 0;
        } catch (Exception e) {
            logger.error("release lock occured an exception : key = {}", key, e);
        } finally {
            // 清除掉ThreadLocal中的数据,避免内存溢出
            lockFlag.remove();
        }
        return false;
    }

}

2.4、进行测试

package com.hlj.redis.lock;

import com.hlj.redis.lock.utils.DistributedLock;
import com.hlj.redis.lock.utils.RedisTool;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import java.util.Date;

/**
 * @Desc:
 * @Author HealerJean
 * @Date 2018/9/13  下午12:04.
 */
@RequestMapping("redis/lock")
@Controller
public class LockController {


    //库存个数
     int goodsCount = 10;

    //卖出个数
     int saleCount = 0;

    /**
     * 缓存key-用户体力锁
     */
    public static final String TEST_LOCK = "test_lock:";

    @Resource
    private DistributedLock lock;

    @GetMapping("test")
    @ResponseBody
    public void lockRedis(){
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {Thread.sleep(2);} catch (InterruptedException e) {}
                if (lock.lock(TEST_LOCK , 3000l, 5, 500)) {
                    if (goodsCount > 0) {
                        goodsCount--;
                        System.out.println("剩余库存:" + goodsCount + " 卖出个数" + ++saleCount);
                    }
                }
                lock.releaseLock(TEST_LOCK);

            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


}

2.5、测试结果

剩余库存:9 卖出个数1
剩余库存:8 卖出个数2
剩余库存:7 卖出个数3
剩余库存:6 卖出个数4
剩余库存:5 卖出个数5
剩余库存:4 卖出个数6
剩余库存:3 卖出个数7
剩余库存:2 卖出个数8
剩余库存:1 卖出个数9
剩余库存:0 卖出个数10





如果满意,请打赏博主任意金额,感兴趣的在微信转账的时候,添加博主微信哦, 请下方留言吧。可与博主自由讨论哦

支付包 微信 微信公众号
支付宝 微信 微信公众号
posted @ 2018-09-13 12:56  HealerJean  阅读(104)  评论(0编辑  收藏  举报