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
如果满意,请打赏博主任意金额,感兴趣的在微信转账的时候,添加博主微信哦, 请下方留言吧。可与博主自由讨论哦
支付包 | 微信 | 微信公众号 |
---|---|---|