要点:
借助中间件Redis的setnx()命令实现分布式锁。setnx()命令只会对不存在的key设值,返回1代表获取锁成功。对存在的key设值,会返回0代表获取锁失败。
public boolean lock1(KeyPrefix prefix, String key, String value, Long lockExpireTimeOut, Long lockWaitTimeOut) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = prefix.getPrefix() + key; Long deadTimeLine = System.currentTimeMillis() + lockWaitTimeOut; for (;;) { String result = jedis.set(realKey, value, "NX", "PX", lockExpireTimeOut); if ("OK".equals(result)) { return true; } lockWaitTimeOut = deadTimeLine - System.currentTimeMillis(); if (lockWaitTimeOut <= 0L) { return false; } } } catch (Exception ex) { log.info("lock error"); } finally { returnToPool(jedis); } return false; }
public boolean unlock1(KeyPrefix prefix, String key, String value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = prefix.getPrefix() + key; String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(luaScript, Collections.singletonList(realKey), Collections.singletonList(value)); if ("1".equals(result)) { return true; } } catch (Exception ex) { log.info("unlock error"); } finally { returnToPool(jedis); } return false; }
一、 redis分布式锁原理
实现原理
获取Redis锁的命令:
SET resource_name my_random_value NX PX 30000
resource_name:资源名称,可根据不同的业务区分不同的锁
my_random_value:随机值,每个线程的随机值都不同,用于释放锁时的校验,最好使用当前线程的id
NX:key不存在的时候设置成功,key存在则设置不成功
PX:自动失效时间,出现异常情况,锁可以过期自动释放
利用NX的原子性,多个线程并发时,只有一个线程可以设置成功,设置成功即可获得锁,可以执行后续的业务处理。
如果出现异常,超过锁的有效期,锁自动释放,释放锁采用Redis的delete命令
释放锁时校验之前设置的随机数,相同才能释放。
二、基于Redis的Setnx实现分布式锁
POM
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
application.properties 配置redis:spring.redis.host=192.168.73.130
package com.example.distributelock.controller; import com.example.distributelock.lock.RedisLock; import com.example.distributelock.lock.ZkLock; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class RedisLockController { @Autowired private RedisTemplate redisTemplate; @RequestMapping("redisLock") public String redisLock(){ log.info("我进入了方法!"); try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){ if (redisLock.getLock()) { log.info("我进入了锁!!"); Thread.sleep(15000); } } catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } log.info("方法执行完成"); return "方法执行完成"; } @RequestMapping("zkLock") public String zkLock(){ log.info("我进入了方法!"); try (ZkLock zkLock = new ZkLock("localhost:2181","order")){ if (zkLock.getLock()) { log.info("我进入了锁!!"); Thread.sleep(15000); } } catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } log.info("方法执行完成"); return "方法执行完成"; } }
Redis的Setnx实现分布式锁操作。
解锁:利用Redis的lua脚本来实现解锁操作的原子性。参考:https://blog.csdn.net/varyall/article/details/117913979
package com.example.distributelock.lock; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.data.redis.core.types.Expiration; import java.util.Arrays; import java.util.List; import java.util.UUID; @Slf4j public class RedisLock implements AutoCloseable { private RedisTemplate redisTemplate; private String key; private String value; //单位:秒 private int expireTime; public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){ this.redisTemplate = redisTemplate; this.key = key; this.expireTime=expireTime; this.value = UUID.randomUUID().toString(); } /** * 获取分布式锁 * @return */ public boolean getLock(){ RedisCallback<Boolean> redisCallback = connection -> { //设置NX RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent(); //设置过期时间 Expiration expiration = Expiration.seconds(expireTime); //序列化key byte[] redisKey = redisTemplate.getKeySerializer().serialize(key); //序列化value byte[] redisValue = redisTemplate.getValueSerializer().serialize(value); //执行setnx操作 Boolean result = connection.set(redisKey, redisValue, expiration, setOption); return result; }; //获取分布式锁 Boolean lock = (Boolean)redisTemplate.execute(redisCallback); return lock; } public boolean unLock() { String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class); List<String> keys = Arrays.asList(key); Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value); log.info("释放锁的结果:"+result); return result; } @Override public void close() throws Exception { unLock(); } }
redis缓存穿透,击穿和雪崩以及解决方案:https://blog.csdn.net/m0_37937394/article/details/122564362
一:redis雪崩
redis雪崩是指redis在某个时间大量失效,突然造成数据库访问压力急剧增大,像雪崩一样,redis雪崩危害巨大,甚至有可能服务器宕机,给公司造成巨大的经济损失。
解决方案:设置超时时间的时候要设置随机值,不要设置固定值
二: redis缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
1.设置并发锁,防止请求大量请求数据库,如果获取到锁了,去数据库查询,如果没有,说明有其他线程在查询数据库,那么只需要重试一下就好了。
public String get(key) { String value = redis.get(key); if (value == null) { //代表缓存值过期 //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 Thread.sleep(50); get(key); //重试 } } else { return value; } }
2.设置拦截器,对于不存在得key,进行拦截
三:缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
1.设置热点数据永不过期。
2.加互斥锁:其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
static Lock reenLock = new ReentrantLock(); public String findPubConfigByKey1(String key) throws InterruptedException { PubConfig result = new PubConfig(); // 从缓存读取数据 result = redisService.getObject(PubConfigKeyConstants.TABLE_NAME + "_"+key, PubConfig.class) ; if (result== null ) { if (reenLock.tryLock()) { try { System.out.println("拿到锁了,从DB获取数据库后写入缓存"); // 从数据库查询数据 result = pubConfigRepository.queryPubConfigInfoByKey(key); // 将查询到的数据写入缓存 Gson g = new Gson(); String value = g.toJson(result); redisService.setNx(PubConfigKeyConstants.TABLE_NAME + "_"+key, value); } finally { reenLock.unlock();// 释放锁 } } else { // 先查一下缓存 result = redisService.getObject(PubConfigKeyConstants.TABLE_NAME + "_"+key, PubConfig.class) ; if (result== null) { System.out.println("我没拿到锁,缓存也没数据,先小憩一下"); Thread.sleep(100);// 小憩一会儿 return findPubConfigByKey1(key);// 重试 } } } return result.getValue(); }
https://baijiahao.baidu.com/s?id=1707897582913572517&wfr=spider&for=pc