spring-boot 基于Redis的分布式锁

为什么要使用分布式锁

     为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

      1.互斥性。在任意时刻,只有一个客户端能持有锁。

      2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

      3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

      4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

redis分布式锁实现代码如下:

package com.zhiku.front.service.Impl;

import com.zhiku.core.utils.ExceptionUtil;
import com.zhiku.front.service.IRedisLockService;
import lombok.extern.slf4j.Slf4j;
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.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分布式锁
 * Created by luohaiqing123 on 2018/12/26.
 */
@Slf4j
@Service("redisLockService")
public class RedisLockService implements IRedisLockService {

    @Autowired
    private RedisTemplate redisTemplate;
    //创建本地线程副本变量(主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰)
    private ThreadLocal lockFlag = new ThreadLocal();
    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();
    }

    /**
     * 获得锁
     * @param key
     * @param expire 锁的有效期 5S(避免死锁,5s后自动释放锁)(请根据自身业务需求设定)
     * @param retryTimes 重试次数10次(避免获得不到锁的情况下,一直不断的获取)(请根据自身业务需求设定)
     * @param sleepMillis 获得锁失败当前线程睡眠时间(请根据自身业务需求设定)
     * @return
     */
    @Override
    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, expire);
        // 如果获取锁失败,按照传入的重试次数进行重试,如果超过获取次数,返回获取失败
        while ((!result) && retryTimes-- > 0) {
            try {
                log.error(key+"获得锁失败, retrying..." + retryTimes);
                Thread.sleep(sleepMillis);
            } catch (Exception e) {
                log.error("获得锁失败 lock error"+ ExceptionUtil.getStackMsg(e));
                return false;
            }
            result = setRedis(key, expire);
        }
        return result;
    }

    /**
     * 上锁方法
     * @param key  锁的key
     * @param expire  锁的有效期
     * @return
     */
    public boolean setRedis(String key,long expire) {
        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);
                }
            });
            log.info("获得锁结果"+result);
            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            log.error("获得锁失败-set redis occured an exception"+ExceptionUtil.getStackMsg(e));
        }
        return false;
    }

    /**
     * 释放锁
     * @param key 锁的key
     * @return
     */
    @Override
    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;
                }
            });
            log.info("解锁结果"+result);
            return result != null && result > 0;
        } catch (Exception e) {
            log.error("释放锁失败-release lock occured an exception", e);
        }
        log.info("解锁失败");
        return false;
    }
}


后续发现这种分布式锁存在问题,两个客户端同时获得锁,详见:
https://mp.csdn.net/postedit/91952397

你可能感兴趣的:(spring-boot,分布式)