作为一个稀有的Java妹子,所写的所有博客都只是当作自己的笔记,留下证据自己之前是有用心学习的~哈哈哈哈(如果有不对的地方,也请大家指出,不要悄悄咪咪的不告诉我)
1.锁
锁是为了控制高并发而产生的,多个线程访问同一个资源、用户多次提交等等都是高并发的情况,带来的影响就是数据不正确。比如提交订单,用户点击很多次,如果代码里不加控制,就会产生多条订单数据。
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量
分布式锁:随着分布式系统的流行,应用被拆分为许多子系统,每个子系统都可能被部署在不同的服务器上,jvm就不止一个,synchronized就不适用于分布式应用了。所以就会考虑在应用服务器之外有一个存储服务器,存储锁信息,是不是就马上想到了redis。还有其他的分布式锁的解决方案,比如zookeeper等。
2.分布式锁的特征
1.锁应当有过期时间,避免加锁服务挂掉后没有释放锁造成死锁。
2.同一时刻只能有一个线程获取到锁。
3.线程只能解自己加的锁
3.redis实现分布式锁
之前的文章中有介绍redis的一个命令,setnx:set if not exists。当key不存在时保存,存在则什么也不做,利用这个命令就可以实现分布式锁。加锁和解锁都是用luna表达式,因为luna表达式可以原子性的执行命令,因为setnx是先set再设置过期时间,如果出现set完后,服务器宕机,过期时间还没设置,就产生了死锁。
加锁的luna语句为:执行成功返回1,表明加锁成功,否则失败,即redis中已存在相同的key了。
"local key = KEYS[1]; local value = ARGV[1]; if redis.call('set', key, value,
'NX' ,'PX', "+ millisecond + ") then return 1 else return 0 end";
解锁的luna语句:查询是否存在key,存在则删除,使用luna保持原子性
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1])
else return 0 end";
完整的service:
@Component
public class RedisLockService {
private static final Long RELEASE_SUCCESS = 1L;
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 加锁
* @param key key
* @param value value
* @param millisecond millisecond
* @param tryCount tryCount
* @return 状态
*/
public boolean lock(String key, String value, int millisecond, int tryCount) {
// 重试次数最大5次
if (tryCount > 5) {
tryCount = 5;
}
try {
String script =
"local key = KEYS[1]; local value = ARGV[1]; if redis.call('set', key, value, 'NX' ,'PX', "
+ millisecond + ") then return 1 else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate
.execute(redisScript, Collections.singletonList(key), Collections.singletonList(value));
// 判断结果
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
} catch (Exception ex) {
// redis命中失败或连接有问题时,重试机制
// 重试次数
if (tryCount == 0) {
return false;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
return false;
}
System.out.println(tryCount);
// 重试机制
return lock(key, value, millisecond, tryCount - 1);
}
return false;
}
/**
* 释放分布式锁
*
* @param lockKey 锁
* @param value 请求标识
* @return 是否释放成功
*/
public boolean releaseLock(String lockKey, String value) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long execute =
redisTemplate.execute(redisScript, Collections.singletonList(lockKey), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(execute)) {
return true;
}
return false;
}
}
4.模拟一下并发情况
public void testRedisLockService() throws InterruptedException {
//加锁,设置过期时间,这里只是为了演示,设置得很大
boolean lock = redisLockService.lock("testLock", "testLock", 1000000000, 0);
try{
if(lock){
log.info("拿到锁,正在处理业务!");
//模拟业务处理,占用锁资源
Thread.sleep(1000000000);
}else {
System.out.println("锁已被占用");
}
}catch (Exception e){
log.info(e.getMessage());
}finally {
//记得释放锁资源
if (lock) {
redisLockService.releaseLock("testLock","testLock");
}
}
}
启动一个线程调用testRedisLockService方法:
再启动一个线程调用testRedisLockService方法:
redis锁起效。
5.注意
在生产环境中,锁的过期时间应该根据业务场景来决定,业务处理完后一定要释放锁资源。
6.缓存穿透、缓存雪崩、缓存击穿
虽然缓存可以提高应用的响应时间,但是还是会有一些潜在的问题,比如数据的一致性,视频的点赞量,缓存里的数据跟数据库的数据肯定不是时时都是一致的,数据库是定时的去同步缓存里的数据,所以是存在数据不一致的。
一般来讲,查询某个数据是先查缓存,如果有则直接返回,没有则查询数据库,数据库中存在则放入缓存,没有则什么也不做。
缓存穿透:指查询时,缓存里没有这个数据,数据库里也没有,那么每次查询都会读库,因为数据库不存在则不会放入缓存,如果攻击者就一直查询这个数据,就会一直访问数据库。比如根据ID查询用户身份,传的是用户ID,用户ID是正整数,如果传一个为-1的就会一直查询数据库,缓存就没有起作用,穿透缓存直接查数据库。
缓存雪崩:某一个时期,缓存里的值集体过期,那么这个时期就会出现数据库压力突然增加的情况,或者是缓存服务器挂机也会出现这种情况。在设置的时候,热点数据过期时间要错开设置过期时间。
缓存击穿:当一个热点key一直被访问,一直承受很大的访问压力,突然这个key不见了,一瞬间所有的压力就落到数据库上,像是在缓存上一个孔被击穿,给数据库造成压力