Redis 分布式锁

一、描述

  一套系统,集群部署,高并发时,访问同一个业务方法,该业务涉及到数据的安全性、准确性,只允许单线程操作,这个时候就需要分布式锁来实现。

  redis实现分布式锁可以采用ValueOperations.setIfAbsent(key, value)或RedisConnection.setNX(key, value)方法

  ValueOperations.setIfAbsent(key, value)封装了RedisConnection.setNX(key, value)

  该方法用意:

    并发或非并发都只允许一个插入成功。

    如果key存在,插入值失败,返回false,可以循环执行,直至成功或超时。

    如果key不存,插入值成功,返回true,业务逻辑处理结束后,将key删除。

二、实现(SpringBoot集成redis)

  1.定义结果类

package com.sze.redis.lock;

/**
 * <br>类 名: LockInfo 
 * <br>描 述: 加锁、释放锁的返回信息
 * <br>作 者: shizhenwei 
 * <br>创 建: 2018年9月10日 
 * <br>版 本: v1.0.0 
 * <br>
 * <br>历 史: (版本) 作者 时间 注释
 */
public class LockInfo {
    private boolean success;
    private String key;
    private String value;
    private String description;
    
    public LockInfo(boolean success, String key, String value) {
        super();
        this.success = success;
        this.key = key;
        this.value = value;
    }
    
    public LockInfo(boolean success, String key, String value, String description) {
        super();
        this.success = success;
        this.key = key;
        this.value = value;
        this.description = description;
    }
    
    public boolean isSuccess() {
        return success;
    }
    public void setSuccess(boolean success) {
        this.success = success;
    }
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}

 

  2.定义接口

package com.sze.redis.lock;

public interface RedisLockManager {
    public static final String ID = "RedisLockManager";
    
    /**
     * <br>描 述: 添加一把锁
     * <br>作 者: shizhenwei 
     * <br>历 史: (版本) 作者 时间 注释
     * @param key 锁的唯一标识
     * @param value 给该key设置一个值,建议使用sessionId,这样你可以通过自己的sessionId删除释放自己的锁
     * @param addTimeOut 添加锁的过程的超时时长,即 时间内  锁没有添加上视为失败 单位毫秒
     * @param lockTimeOut 添加锁成功后,该锁的有效时长,即 超过该时长 锁自动放弃 单位毫秒         建议 大于 真实业务逻辑处理时长
     * @return
     */
    LockInfo addlock(String key,String value,int addTimeOut,int lockTimeOut);

    
    /**
     * <br>描 述:    放弃 释放锁
     * <br>作 者: shizhenwei 
     * <br>历 史: (版本) 作者 时间 注释
     * @param key 锁的唯一标识
     * @param value 校验值是否正确,正确则允许释放
     * @return
     */
    LockInfo freeLock(String key,String value);
    
    /**
     * <br>描 述: 强制占有一把锁,如果这把锁之前有线程占有,可能会出现高并发导致的数据错误
     * <br>作 者: shizhenwei 
     * <br>历 史: (版本) 作者 时间 注释
     * @param key 锁的唯一标识
     * @param value 给该key设置一个值,建议使用sessionId,这样你可以通过自己的sessionId删除释放自己的锁
     * @param lockTimeOut 添加锁成功后,该锁的有效时长,即 超过该时长 锁自动放弃 单位毫秒        建议 大于 真实业务逻辑处理时长
     * @return
     */
    LockInfo enforceAddlock(String key,String value,int lockTimeOut);
    
    
    /**
     * <br>描 述:    强制 放弃 释放锁,如果这把锁之前有线程占有,释放后,有可能会出现高并发导致的数据错误
     * <br>作 者: shizhenwei 
     * <br>历 史: (版本) 作者 时间 注释
     * @param key 锁的唯一标识
     * @return
     */
    LockInfo enForceFreeLock(String key);
}

 

  3.实现类

package com.sze.redis.lock;

import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service(RedisLockManager.ID)
public class RedisLockManagerImpl implements RedisLockManager {
    
    @Autowired
    private StringRedisTemplate redisTampate;
    
    private ValueOperations<String, String> stringRedis;
    
    /**
     * <br>描 述: 初始化时赋值
     * <br>作 者: shizhenwei 
     * <br>历 史: (版本) 作者 时间 注释
     */
    @PostConstruct
    private void init(){
        stringRedis = redisTampate.opsForValue();
    }
    
    /**
     * 添加分布式锁
     * key 建议全局常量定义
     * value 建议使用sessionId
     * addTimeOut 毫秒
     * lockTimeOut 毫秒 建议大于 真实业务逻辑处理时长
     * 详解见接口注解
     */
    @Override
    public LockInfo addlock(String key, String value, int addTimeOut, int lockTimeOut) {
        //StringUtils是spring framework自带的工具类,功能还是挺齐全的,建议开发中使用该类或者继承、copy其类
        if(!StringUtils.hasText(key) || !StringUtils.hasText(value)){
            return new LockInfo(false, key, value,"key和value不能为空");
        }
        
        if(addTimeOut<100 || lockTimeOut<100){
            return new LockInfo(false, key, value,"addTimeOut和lockTimeOut不能小于100毫秒");
        }
        
        //setIfAbsent执行的底层的RedisConnection.setNX方法,分布式锁的判定也是通过该方法
        Boolean b = stringRedis.setIfAbsent(key, value);//该key有值,返回false,意为该key在被其他分布式锁定占用
        if(b){
            //获取到锁
            redisTampate.expire(key, lockTimeOut, TimeUnit.MILLISECONDS);//设置锁的有效时长
            return new LockInfo(true, key, value,"锁添加成功");        
        }else{
            //循环获取锁,直到获取到,或者超市
            long timeOut = System.currentTimeMillis()+addTimeOut;
            while(System.currentTimeMillis()<timeOut){
                try {
                    //之所以设置睡眠,目的为了降低jvm压力
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    return new LockInfo(false, key, value,"线程异常");
                }
                //setIfAbsent执行的底层的RedisConnection.setNX方法,分布式锁的判定也是通过该方法
                Boolean b2 = stringRedis.setIfAbsent(key, value);
                if(b2){
                    redisTampate.expire(key, lockTimeOut, TimeUnit.MILLISECONDS);//设置锁的有效时长
                    return new LockInfo(true, key, value,"锁添加成功,之前有线程在占用");
                }
            }
            return new LockInfo(false, key, value,"锁正在被其他线程持续占用中,获取锁的请求超时");
        }
    }
    
    /**
     * 释放分布式锁
     * key 建议全局常量定义
     * value 建议使用sessionId
     * 详解见接口注解
     */
    @Override
    public LockInfo freeLock(String key, String value) {
        //StringUtils是spring framework自带的工具类,功能还是挺齐全的,建议开发中使用该类或者继承、copy其类
        if(!StringUtils.hasText(key) || !StringUtils.hasText(value)){
            return new LockInfo(false, key, value,"key和value不能为空");
        }
        if(value.equals(stringRedis.get(key))){
            redisTampate.delete(key);
            return new LockInfo(true, key, value,"锁释放成功");
        }else{
            return new LockInfo(false, key, value,"value不对,无权释放锁");
        }
    }
    
    
    /**
     * 强制添加分布式锁
     * key 建议全局常量定义
     * value 建议使用sessionId
     * lockTimeOut 毫秒 建议大于 真实业务逻辑处理时长
     * 详解见接口注解
     */
    @Override
    public LockInfo enforceAddlock(String key, String value,int lockTimeOut) {
        //StringUtils是spring framework自带的工具类,功能还是挺齐全的,建议开发中使用该类或者继承、copy其类
        if(!StringUtils.hasText(key) || !StringUtils.hasText(value)){
            return new LockInfo(false, key, value,"key和value不能为空");
        }
        
        if(lockTimeOut<100){
            return new LockInfo(false, key, value,"addTimeOut和lockTimeOut不能小于100毫秒");
        }
        stringRedis.set(key, value, lockTimeOut, TimeUnit.MILLISECONDS);
        return new LockInfo(true, key, value,"锁强制添加成功");
    }
    
    /**
     * 强制释放分布式锁
     * key 建议全局常量定义
     * 详解见接口注解
     */
    @Override
    public LockInfo enForceFreeLock(String key) {
        //StringUtils是spring framework自带的工具类,功能还是挺齐全的,建议开发中使用该类或者继承、copy其类
        if(!StringUtils.hasText(key)){
            return new LockInfo(false, key, null,"key不能为空");
        }
        redisTampate.delete(key);
        return new LockInfo(true, key, null,"锁释放成功");
    }
}

 

  4.定义常量类

package com.sze.redis.lock;

/**
 * <br>类 名: LockConstants 
 * <br>描 述: 各个流程需要获取锁,释放锁的key、获取锁的超时时长(毫秒)、拿到锁后的超时时长(毫秒)
 * <br>作 者: shizhenwei 
 * <br>创 建: 2018年9月10日 
 * <br>版 本: v1.0.0 
 * <br>
 * <br>历 史: (版本) 作者 时间 注释
 */
public class LockConstants {
    //默认key
    public static final String DEFAULT_KEY = "DEFAULT_KEY";
    //添加锁时的默认时长,即 开始——添加锁成功的超时时长
    public static final long DEFAULT_ADDTIME = 3_000;
    //锁添加成功后的有效时长,即 该时长过后,锁自动放弃
    public static final long DEFAULT_LOCKTIME = 3_000;
    
    
    //购买流程——锁——key,分布式多应用,高并发时,获取该key的所
    public static final String BUY_DINGJIE_KEY = "BUY_DINGJIE_KEY";
    
    //获取购买流程锁时的超时长
    public static final int BUY_DINGJIE_ADDTIME = 10_000;
    
    //获取到购买流程锁后的有效时长
    public static final int BUY_DINGJIE_LOCKTIME = 10_000;
}

  

  5.测试类——模拟多线程

package com.sze.redis.lock;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisLockManagerTest {
    
    @Autowired
    private RedisLockManager redisLockManager;
    
    /**
     * <br>描 述: 购买丁洁
     * <br>作 者: shizhenwei 
     * <br>历 史: (版本) 作者 时间 注释
     */
    public void buy(){
        //这个value值,建议使用sessionId,这里为了测试,使用了一个妹子的名字
        //先取得锁
        LockInfo lockInfo = redisLockManager.addlock(LockConstants.BUY_DINGJIE_KEY, "丁洁", LockConstants.BUY_DINGJIE_ADDTIME, LockConstants.BUY_DINGJIE_LOCKTIME);
        //处理业务
        if(lockInfo.isSuccess()){
            //成功业务
            System.out.println("============"+Thread.currentThread().getName()+"===========");
            System.out.println(lockInfo.getDescription());
            System.out.println("开始购买丁洁业务。。。");
            System.out.println("购买丁洁业务结束。。。");
            //释放锁
            LockInfo lockInfo2 = redisLockManager.freeLock(LockConstants.BUY_DINGJIE_KEY, "丁洁");
            System.out.println(lockInfo2.getDescription());
        }else{
            //失败业务
            System.out.println("============"+Thread.currentThread().getName()+"===========");
            System.out.println(lockInfo.getDescription());
            System.out.println("购买丁洁业务失败。。。");
        }
    }
    
    @Test
    public void test(){
        //测试前先把锁释放掉
        redisLockManager.enForceFreeLock(LockConstants.BUY_DINGJIE_KEY);
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                 buy();
            }
        },"线程1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                 buy();
            }
        },"线程2");
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                 buy();
            }
        },"线程3");
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                 buy();
            }
        },"线程4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t1.run();
        t2.run();
        t3.run();
        t4.run();
    }
    
}

  

  6.输出结果

 

 注:关于SpringBoot集成redis,参考:https://www.cnblogs.com/zwcry/p/9633184.html 

 

 

 

posted @ 2018-09-11 17:31  七脉  阅读(1020)  评论(0编辑  收藏  举报