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