redis lock优化(保证redis值、过期时间原子性以及删除锁原子操作)

分布式锁用 Redis 还是 Zookeeper?https://mp.weixin.qq.com/s/Fw54yeKOY6mAlqJyx7HTDw

package com.xxx.xxx;//你的包名

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
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;

/**
 * @author Jiangjunjie
 * @Date 2020/7/7
 * 通过给set NX(一个有锁,其他线程不能再获取锁),PX(设置锁的自动过期时间) 进行保证redis的值以及过期时间的原子性
 * 通过给锁设置一个拥有者的标识,即每次在获取锁的时候,生成一个随机不唯一的串放入当前线程,释放锁的时候先去判断对应的值是否和线程中的值相同(使用lua脚本)
 * 避免删除了其他锁
 */

@Component
public class RedisLock {
    /* add by jiangjunjie Task 1502理财分布式锁优化 start */
    private RedisTemplate redisTemplate = new RedisTemplate<>();

    private String UNLOCK_LUA;

    private ThreadLocal lockFlag = new ThreadLocal();

    @Autowired
    public RedisLock(RedisTemplate redisTemplate)
    {
        // 通过Lua脚本来达到释放锁的原子性
        if("".equals(this.UNLOCK_LUA) || this.UNLOCK_LUA==null )
        {
            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 ");
            this.UNLOCK_LUA = sb.toString();
        }
        this.redisTemplate=redisTemplate;
    }

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

    private boolean setRedis(String key, long expire) {
        //为了保证设置锁和过期时间的两个操作原子性 spring data 的 RedisTemplate当中没有这样的方法,但是jedis当中有这样的原子操作的方法
        //需要通过RedisTemplate的execute方法获取jedis里操作命令对象

        // NX:表示只有当锁定资源不存在的时候才能set成功。利用Redis的原子性,保证了只有第一个请求的线程才能获得锁,而后其他线程在锁定资源释放前都不能获取锁
        // PX:expire表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定。

        //通过set NX,PX的命令设置保证了Redis值和自动过期时间的原子性,避免在调用setIfAbsent方法的时候线程挂掉,没有设置过期时间而导致死锁,使得锁不能释放
        try {
            String result = redisTemplate.execute(new RedisCallback() {
                @Override
                public String doInRedis(RedisConnection connection) throws DataAccessException {
                    JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                    String uuid = UUID.randomUUID().toString();
                    lockFlag.set(uuid); // 锁定的资源
                    return commands.set(key, uuid, "NX", "PX", expire);
                }
            });
            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return false;
    }

    /*上面的方法通过设置set的NX,PX命令保证了Redis值和自动过期时间的原子性,但是还有一个问题是如果线程T1获取锁,但是在处理T1的业务时候,
    由于某些原因阻塞了较长时间,这个时候设定的过期时间到了,线程T2获取了锁,线程T1操作完后释放了锁(释放了T2的锁)
    所以也就是说T2的线程上面没有提供锁的保护机制。因此需要给锁定一个拥有者的标识,即每次在获取锁的时候,生成一个随机不唯一的串放入当前线程,
    释放锁的时候先去判断对应的值是否和线程中的值相同。*/
    public boolean releaseLock(String key) {
        // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
        try {
            List keys = new ArrayList();
            keys.add(key);
            List args = new ArrayList();
            args.add(lockFlag.get());
            // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
            Long result = redisTemplate.execute(new RedisCallback() {
                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) {
            e.printStackTrace();
        }
        return false;
    }
    /* add by jiangjunjie Task1502 理财分布式锁优化 end */
}


可参考原文档 https://my.oschina.net/dengfuwei/blog/1600681

你可能感兴趣的:(Java学习)