单机锁:
多线程访问redis缓存时, 要保证数据同步,可以给redis里的数据加锁。
加锁有两种方式, 一种是使用java 的关键字 synchronized , 一种是使用ReentratLock类。
synchronized加锁手段非常强硬,一旦锁住,除非块内程序执行完成,否则不开锁。其他线程想要访问就得一直等待。
而RenntraLock类的加锁方式比较温和。 一个线程使用RenntraLock去抢锁, 抢到了就执行代码, 抢不到可以在else中处理抢不到锁的代码。 还可以设置超时时间,抢不到的话,在指定的时间内继续抢,时间一到,则不再抢锁。 和synchronized不同,synchronized抢不到会一直抢,直到抢到才罢休。
RenntraLock代码示例:
Lock lock = new ReentrantLock();
if(lock.tryLock())
{
lock.lock();
//业务逻辑
}
else
{
// 没有抢到线程的业务逻辑--放弃执行
lock.unlock();
}
synchronized代码示例:
synchronized(this)
{
}
分布式锁:
synchronized和ReentratLock只适用于单例服务,并不适用于微服务模式。 synchronized 和 ReentratLock 加锁的作用域只是当前服务。 在当前服务内的多进程下可以保证数据唯一性。但是当在分布式环境下,有多个相同的服务部署在不同服务器时, 使用redis会出现风险。 synchronized不可能锁住好几个服务的数据。 此时需要使用redis自带的set方法来完成锁数据的操作。
setIfAbsent方法提供在分布式环境下对数据的加锁处理。
/**
* stringRedisTemplate 提供setIfAbsent方法对指定的内容加锁。
* 适用于分布式架构
* 按照唯一键lockKey加锁, 返回boolean类型的加锁结果
* setIfAbsent表示如果已经存在lockKey的锁,则不会继续加锁,返回false表示加锁失败
* 如果不存在lockKey的锁,则对value进行加锁,然后执行业务代码
*/
String lockKey = UUID.randomUUID().toString()+ "serverName";
String value = "被加锁的内容";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey , value);
if(!flag)
{
// 加锁失败
}
else
{
//指定业务代码后,释放锁
stringRedisTemplate.delete(lockKey);
}
上述代码可以完成分布式加锁,但存在一个问题。 如果程序执行到业务代码中时,抛出异常了怎么办? 应为解锁代码在业务代码之后,也就是多要在业务代码执行完成后在解锁。 但是如果业务代码指定一半是抛出了异常,那么最后的解锁代码也执行不到。
为了解决这个问题, 需要对从加锁开始到执行业务代码 进行 try-catch处理。 在finally语句中释放锁。 这样不论业务代码有没有抛出异常,最后都会解锁。
/**
* stringRedisTemplate 提供setIfAbsent方法对指定的内容加锁。
* 适用于分布式架构
* 按照唯一键lockKey加锁, 返回boolean类型的加锁结果
* setIfAbsent表示如果已经存在lockKey的锁,则不会继续加锁,返回false表示加锁失败
* 如果不存在lockKey的锁,则对value进行加锁,然后执行业务代码
*/
String lockKey = UUID.randomUUID().toString()+ "serverName";
String value = "被加锁的内容";
try
{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey , value);
if(!flag)
{
// 加锁失败
}
else
{
//执行业务代码
}
}
catch (Exception e)
{
}
finally {
stringRedisTemplate.delete(lockKey);
}
return ResponseResult.ok(message);
}
在解决了程序因为抛异常而无法执行到解锁语句的问题后, 还有一个问题没有解决。 那就是: 如果程序执行到业务代码,但是整个宕机了,整个服务挂掉了,可能是谁杀了服务的进行。这时候哪怕是有finally语句,因为进程被杀了,也无法执行到。 但是redis还正常运行。 被加锁的数据因此无法解锁。 这时候该怎么办? 所以,为了因对这种情况, 在对数据加锁时, 需要对数据指定超时时间。 指定超时时间后,即时服务宕机无法执行到解锁语句,在redis中加锁的数据也有一个超时时间,超时后对自动删除。
// 给被加锁数据设置一个超时时间
stringRedisTemplate.expire(lockKey , 10, TimeUnit.SECONDS);
// 要保证加锁和给所一个时间 是一个原子操作
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey , value , 10 , TimeUnit.SECONDS);