1.锁 2.分布式 3.单机Redis锁基于RedisTemplate
锁
1.在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。
2.而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
3.不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如java中synchronize是在对象头设置标记,Lock接口的实现类基本上都只是某一个volitile修饰的int型变量其保证灭个线程都能拥有对该int的可见性和原子修改,linux内核中也是利用互斥量或信号量等内存数据做标记。
4.除了利用内存数据做锁其实任何互斥的都能做锁(只考虑互斥情况),如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等。只需要满足在对标记进行修改能保证原子性和内存可见性即可。
分布式
分布式情况
此处主要指集群模式下,多个相同服务同时开启.
1.分布式与单机情况下最大的不同在于其不是多线程而是多进程。
2.多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。
分布式锁
1.当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
2.与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。(我觉得分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。。。一个大坑)
3.分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。
单机Redis锁
基本锁
1.原理:利用Redis的setnx如果不存在某个key则设置值,设置成功则表示取得锁成功。
2.缺点:如果获取锁后的进程,在还没执行完的时候挂调了,则锁永远不会释放。
改进型
1.改进:在基本型是锁上的setnx后设置expire,保证即使获取锁的进程不主动释放锁,过一段时间后也能自动释放。
2.缺点:
1.setnx与expire不是一个原子操作,可能执行完setnx该进程就挂了。
2.当锁过期后,该进程还没执行完,可能造成同时多个进程取得锁。(貌似这个问题目前还没有很优雅的解决方案)
总结
一般情况下直接用setnx加expire就够了,但从安全性的角度看还是存在一下几个问题:
1.单点问题。单机Redis只在单机上,如果单机down了,那么所有需要用分布式锁的地方均获取不到锁,全部阻塞。需要做好降级的处理。
2.可能出现多进程同时拥有锁。
RedisLock实现代码(基于GitHub源代码修改)
public class RedisLock {
private static Logger logger = LoggerFactory.getLogger(RedisLock.class);
private RedisTemplate redisTemplate;
private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;
/**
* LockExp.Lock key path.
*/
private String lockKey;
/**
* 锁超时时间,防止线程在入锁以后,无限的执行等待
*/
private int expireMsecs = 60 * 1000;
/**
* 锁等待时间,防止线程饥饿
*/
private int timeoutMsecs = 10 * 1000;
private volatile boolean locked = false;
/**
* Detailed constructor with default acquire timeout 10000 msecs and lock
* expiration of 60000 msecs.
*
* @param lockKey
* lock key (ex. account:1, ...)
*/
public RedisLock(RedisTemplate redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey + "_lock";
}
/**
* Detailed constructor with default lock expiration of 60000 msecs.
*
*/
public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) {
this(redisTemplate, lockKey);
this.timeoutMsecs = timeoutMsecs;
}
/**
* Detailed constructor.
*
*/
public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
this(redisTemplate, lockKey, timeoutMsecs);
this.expireMsecs = expireMsecs;
}
/**
* @return lock key
*/
public String getLockKey() {
return lockKey;
}
private String get(final String key) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] data = connection.get(serializer.serialize(key));
connection.close();
if (data == null) {
return null;
}
return serializer.deserialize(data);
}
});
} catch (Exception e) {
logger.error("get redis error, key : {}", key);
}
return obj != null ? obj.toString() : null;
}
//不存在则设置值(1),存在则设置失败(0)
private boolean setNX(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
connection.close();
return success;
}
});
} catch (Exception e) {
logger.error("setNX redis error, key : {}", key);
}
return obj != null ? (Boolean) obj : false;
}
//设置并返回前面的值
private String getSet(final String key, final String value) {
Object obj = null;
try {
obj = redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
StringRedisSerializer serializer = new StringRedisSerializer();
byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
connection.close();
return serializer.deserialize(ret);
}
});
} catch (Exception e) {
logger.error("setNX redis error, key : {}", key);
}
return obj != null ? (String) obj : null;
}
/**
* 获得 lock. 实现思路:
* 主要是使用了redis 的setNX命令,缓存了锁. reids缓存的key是锁的key,
* 所有的共享,value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间) 执行过程:
* 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
* 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
*
* @return true if lock is acquired, false acquire timeouted
* @throws InterruptedException
* in case of thread interruption
*/
public synchronized boolean lock() throws InterruptedException {
//锁等待时间
int timeout = timeoutMsecs;
while (timeout >= 0) {
// 锁到期时间
long expires = System.currentTimeMillis() + expireMsecs + 1;
String expiresStr = String.valueOf(expires);
//如果存在key则可以不进入(已存在设置value) 设置localkey的运行时间
if (this.setNX(lockKey, expiresStr)) {
// lock acquired
locked = true;
return true;
}
// redis里的时间
String currentValueStr = this.get(lockKey);
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的.
// lock is expired
String oldValueStr = this.getSet(lockKey, expiresStr);
// 获取上一个锁到期时间,并设置现在的锁到期时间,
// 只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的.
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 防止误删(覆盖,因为key是相同的)了他人的锁
// ——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
// [分布式的情况下]:
// 如果这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
// lock acquired
locked = true;
return true;
}
}
timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
/*
* 延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
* 只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
* 使用随机的等待时间可以一定程度上保证公平性
*/
Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
}
return false;
}
/**
* Acqurired lock release.
*/
public synchronized void unlock() {
if (locked) {
redisTemplate.delete(lockKey);
locked = false;
}
}
}
主要思路:
1.setNX(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,
如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
3. 计算newExpireTime=当前时间+过期超时时间,
然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
4. 判断currentExpireTime与oldExpireTime 是否相等,
如果相等,说明当前getset设置成功,获取到了锁。
如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
5. 在获取到锁之后,当前线程可以开始自己的业务处理,
当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,
如果小于锁设置的超时时间,则直接执行delete释放锁;
如果大于锁设置的超时时间,则不需要再锁进行处理。
简要思路:
RedisTemplate redisTemplate; (redis模板对jedis的封装)
DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;(判定的时间)
String lockKey; (锁的名称,即是分布式锁的key)
int expireMsecs (锁超时时间,防止无限时间等待)
int timeoutMsecs (锁等待时间,防止线程饥饿)
volatile boolean locked = false;
get( key ) 通过key值得到value值
setNX( key ) 设置key的值,存在则返回0,不存在则返回1.
getSet( key ) 设置key的值并返回通过key设置的value值.
主要涉及思路
lock()
针对每一个竞争的线程,通过延迟DEFAULT_ACQUIRY_RESOLUTION_MILLIS时间(在等待时间内迭代减)
不在的情况下使用setNX,快速设置.快速使用setNx判定是否有localkey
存在的情况下判定通过判定当前时间的值来判断过期,设置当前的超期时间
unlock()
删除key的值
客户端实现:
public class RedisLockTest {
public static void main(String[] args)
{
for (int i = 0; i < 20; i++) { //开启线程竞争单机Redis
new Thread(new Runnable() {
public void run() {
task(Thread.currentThread().getName());
}
}).start();
}
}
private static void task(String name){
RedisTemplate<Serializable,Serializable> rt = new RedisTemplate();
String localKey = new String ("MyLock");
RedisLock rlock = new RedisLock(rt,localKey,10000,60000);
JedisConnectionFactory con = new JedisConnectionFactory(); //设置redisTemplate的地址等
con.setHostName("客户端Host地址");
con.setPort(6379);
con.setPassword("Host密码");
con.setUsePool(true);
con.afterPropertiesSet();
rt.setConnectionFactory(con);
rt.afterPropertiesSet();
try {
rlock.lock();
if (rlock.lock()) {
System.out.println(name + ":on task");
rlock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}