要点:
借助中间件Redis的setnx()命令实现分布式锁。setnx()命令只会对不存在的key设值,返回1代表获取锁成功。对存在的key设值,会返回0代表获取锁失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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 ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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
1 2 3 4 | <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> |
application.properties 配置redis:spring.redis.host=192.168.73.130
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | 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.设置并发锁,防止请求大量请求数据库,如果获取到锁了,去数据库查询,如果没有,说明有其他线程在查询数据库,那么只需要重试一下就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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.加互斥锁:其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
2016-07-04 MVC Pager使用