项目中经常使用到redis锁,锁最常用的场景在多线程操作共享资源时,需要对共享资源进行加锁,避免造成重复处理或处理时数据已经是脏数据.多个线程使用同一个锁,也就是锁必须独立与这些线程之外(也可以使用线程直接变量共享,用的少),在一个独立的应用中,锁可以直接存储在应用上,这样这个应用的其他线程都可以获取到,但是一旦牵扯到多个应 用以分布式的形式存在,这就需要分布式锁,而redis锁就是一种应用很广泛的分布式锁.
达到分布式锁的目的有多种,DB锁就是其中一种,DB锁对一条记录加锁,多个连接竞争访问,而redis基于单进程单线程模式,采用队列模式将并发访问变成串行访问,多个客户端对redis的连接不存在竞争关系.
redis实现业务锁的一种简单的方式:获取锁=key存在,加锁=设置key,释放锁=删除key
redis加锁,获取锁都是采用set指令,set指令有set,setinx,setex,psetex,不过后三种都可以通过参数以set的方式实现(redis set指令)
- EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
- PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
- NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
- XX :只在键已经存在时,才对键进行设置操作。
package spring.redis.test;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.springframework.util.CollectionUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Transaction;
public class TestRedis {
public static boolean unLock(JedisPool pool,String key,String token) {
Jedis jedis = pool.getResource();
try {
String tok = jedis.get(key);
if (token.equals(tok)) {
//释放锁
Long del = jedis.del(key);
if (del != null && del > 0) {
return true;
} else {
return false;
}
}
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
if (jedis != null) {
jedis.close();
System.out.println(Thread.currentThread().getName()+"unlock close jedis");
}
}
return false;
}
public static String getLock(JedisPool pool,String key) {
Jedis jedis = pool.getResource();
String token = UUID.randomUUID().toString();
String returnValue = null;
Transaction multi = null;
try {
if (jedis.exists(key)) {
System.out.println(Thread.currentThread().getName()+" lock held by other!");
} else {//多线程时可能同时进入此
jedis.watch(key);
multi =jedis.multi();
multi.setnx(key, token);//result 只会返回0和1,0-已存在不做处理,1-不存在并设值
multi.expire(key, 60*60);
List<Object> exec = multi .exec();
//jedis.unwatch();执行了exec就没必须要再执行了
//exec执行成功exec={1,1},执行失败exec.size=0
if (!CollectionUtils.isEmpty(exec)) {
returnValue = token;
}
}
return returnValue;
} catch (Exception e) {
System.out.println(Thread.currentThread().getName()+" error to connect redis");
e.printStackTrace();
return returnValue;
} finally {
if (jedis != null) {
jedis.close();
System.out.println(Thread.currentThread().getName()+"lock close jedis");
}
if (multi != null) {
try {
multi.clear();
multi.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(20);
config.setMaxTotal(40);
config.setMinIdle(10);
final JedisPool pool = new JedisPool(config, "localhost", 6379, 60 * 60);
final String key="test";
Thread[] threads = new Thread[2];
for (int i = 0; i < 2; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
String token = getLock(pool,key);
if (token == null) {//没有获取到锁
System.out.println("do not get lock");
} else {
//deal business
if (unLock(pool, key, token)) {//防止过期锁 删除key
System.out.println("success to unlock");
} else {
System.out.println("error to unlock");
}
}
}
});
}
for (Thread thread : threads) {
thread.start();
}
TimeUnit.SECONDS.sleep(5);
if (pool !=null) {
pool.close();
System.out.println("close pool");
}
}
}
这里有个小疑惑,使用@Test注解测试 多线程总是报read Timeout,找不到原因(执行jedis.setnx())-有时间再回过头去看看.
package spring.redis.test;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class TestRedis {
public static boolean unLock(JedisPool pool,String key,String token) {
Jedis jedis = pool.getResource();
try {
String tok = jedis.get(key);
if (token.equals(tok)) {
//释放锁
Long del = jedis.del(key);
if (del != null && del > 0) {
return true;
} else {
return false;
}
}
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
if (jedis != null) {
jedis.close();
System.out.println(Thread.currentThread().getName()+"close jedis");
}
}
return false;
}
public static String getLock(JedisPool pool,String key) {
Jedis jedis = pool.getResource();
String token = UUID.randomUUID().toString();
String returnValue = null;
try {
String result = jedis.set(key, token,"NX","EX",60 *60);//返回 OK 或NULL
if ("OK".equalsIgnoreCase(result)) {//不存在,享有处理资源的权利,即获取到锁
System.out.println("success lock!");
returnValue = token;
} else {//已存在()
System.out.println("error lock!");
}
return returnValue;
} catch (Exception e) {
System.out.println(Thread.currentThread().getName()+" error to connect redis");
e.printStackTrace();
return returnValue;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public static void main(String[] args) throws InterruptedException {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(20);
config.setMaxTotal(40);
config.setMinIdle(10);
final JedisPool pool = new JedisPool(config, "localhost", 6379, 60 * 60);
final String key="test";
Thread[] threads = new Thread[2];
for (int i = 0; i < 2; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
String token = getLock(pool,key);
if (token == null) {//没有获取到锁
System.out.println("do not get lock");
} else {
//deal business
if (unLock(pool, key, token)) {//防止过期锁 删除key
System.out.println("success to unlock");
} else {
System.out.println("error to unlock");
}
}
}
});
}
for (Thread thread : threads) {
thread.start();
}
TimeUnit.SECONDS.sleep(5);
if (pool !=null) {
pool.close();
System.out.println("close pool");
}
}
}
此处将判断存在,同时设置时间放在一起了,也就不需要事务支持了
再来看看用redisTemplate实现加锁,模板没有实现支持set(key,value,nxxx,pxxx,expire)(猜的),因此只能采用第一种方法.第一种方法钟使用token验证,采用token程序更健壮,防止过期锁删却还能删除.举一个应用场景:A获取taskLock,开始执行task,但锁的时间设置太短,任务没执行完,锁已经失效了,B又获取到了taskLock,但A却可以删除B刚刚获取到的锁.这样就失去了锁的意义了.项目中通常设置足够长的过期时间,一旦获取到任务锁,在过期时间内足够执行任务完毕,同时删除锁,这样就省去了进行token验证.代码如下:
package spring.redis.test.util;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean lock(String key, String value, int expire) {
try {
redisTemplate.watch(key);
if (redisTemplate.hasKey(key)) {
return false;
}
redisTemplate.multi();
ValueOperations<String, String> vo = redisTemplate.opsForValue();
if (expire != -1) {
vo.set(key, value, expire, TimeUnit.SECONDS);
} else {
vo.set(key, value);
}
// ############################
/*
* 此行用于判断是否执行成功,因为ValueOperations.set直接回调,exec()获取不到其执行结果
*/
redisTemplate.getExpire(key);
// ############################
List<Object> exec = redisTemplate.exec();
if (CollectionUtils.isEmpty(exec)) {
return false;
}
return true;
} catch (Exception e) {
return false;
} finally {
RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
}
}
public void unlock(String key) {
redisTemplate.delete(key);
}
}
单元测试
@Test
public void set(){
if (redisUtil.lock("test", "", 60)) {
// deal business
redisUtil.unlock("test");
System.out.println("end");
} else {
System.out.println("fail to get lock!");
}
}