单节点redis下分布式锁。
- 原理:使用setnx,设置成功返回1,失败返回0,由于redis也是单线程的,所以一次只能有一个线程获取成功。
- 程序异常情况:设置超时时间,避免程序挂掉锁无法释放。
- 执行超时情况:为避免代码运行时间超过key设置的超时时间,从而释放了其他进程的锁问题。需要保存当前线程的value。在释放之前先检查key设置的value是否跟当前相等,类似CAS的思想,比较再删除。
- 资源释放:如果查询key的value与当前设置的value相等,再进行删除。这两步不是原子操作的,也会有问题。比如两个操作之前有网络延迟。 可以使用lua脚本来释放锁。
分布式锁代码如下:
public class RedisDscLock {
/**
* 设置key过期时间
*/
private int lockExpireTime = 2;
/**
* 加锁成功后返回value
*
* @param key
* @param timeOutInSecond
* @return
*/
public String getLock(Jedis jedis, String key, int timeOutInSecond) throws TimeoutException {
//设置成功返回1,否则返回0
timeOutInSecond = timeOutInSecond * 1000;
SetParams params = new SetParams();
params.nx().ex(lockExpireTime);
Long startTime = System.currentTimeMillis();
String value = null;
key = getLockKey(key);
value = UUID.randomUUID().toString();
while (System.currentTimeMillis() - startTime < timeOutInSecond) {
String res = jedis.set(key, value, params);
if ("OK".equals(res)) {
System.out.println(Thread.currentThread().getName() + " get lock");
return value;
}
}
//用完之后,必须释资源,否则会导致无连接池可用。
throw new TimeoutException("获取锁超时");
}
private String getLockKey(String key) {
return "lock:" + key;
}
/**
* 执行结束的脚本
*/
private static String lua = " if redis.call(\"get\",KEYS[1])== ARGV[1] " +
"then return redis.call(\"del\",ARGV[1]) " +
"else return 0 " +
"end";
/**
* 使用lua脚本 查询和删除的原子操作
*
* @param key
* @param value
* @return
*/
public Long releaseLockByLua(Jedis jedis, String key, String value) {
if (value == null) {
return 0L;
}
System.out.println(Thread.currentThread().getName() + " release lock");
key = getLockKey(key);
long res = (Long) jedis.eval(lua, Arrays.asList(key), Arrays.asList(value));
System.out.println(Thread.currentThread().getName() + " release lock success");
jedis.close();
return res;
}
}
测试代码
public class RedisLockDemo {
private static ExecutorService pool = new ThreadPoolExecutor(10, 20, 5L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
private static String serverIp = "xxx";
private static volatile int goodsNum = 100;
private static int threadNum = 20;
private static CountDownLatch countDownLatch = new CountDownLatch(threadNum);
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(100);
config.setMinIdle(10);
config.setTestOnBorrow(false);
config.setTestOnReturn(false);
config.setTestOnCreate(false);
config.setBlockWhenExhausted(true);
config.setMaxWaitMillis(1000);
jedisPool = new JedisPool(config, serverIp);
}
public static void main(String[] args) throws InterruptedException, TimeoutException, BrokenBarrierException {
RedisDscLock lock = new RedisDscLock();
for (int tNum = 0; tNum < threadNum; tNum++) {
pool.submit(new Runnable() {
@Override
public void run() {
String value = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
System.out.println(Thread.currentThread().getName() + "buying");
value = lock.getLock(jedis, "goodsNum", 100);
goodsNum--;
System.out.println("after buging" + goodsNum);
} catch (TimeoutException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
lock.releaseLockByLua(jedis, "goodsNum", value);
} catch (Exception e) {
e.printStackTrace();
}
if (jedis != null) {
jedis.close();
}
countDownLatch.countDown();
}
}
});
}
//等待所有线程执行完毕
countDownLatch.await();
System.out.println("remain goods" + goodsNum);
pool.shutdown();
}
}
只是一个实验版本,您要是有问题欢迎留言沟通。