分佈式鎖主要用於解決,公司中不一樣業務系統對同一功能的數據產生髒讀或重複插入。html
好比公司現有三個小組分別開發WAP站、小程序、APP客戶端,而這三個系統都存在領紅包功能。java
業務要求每人每日只能領取一個紅包,若是有人同時登錄三個系統那麼就可以同一時間領取到三個紅包。git
分佈式鎖要知足如下基本要求:github
本例參考了博文:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/redis
例子已上傳碼雲:https://gitee.com/imlichao/jedis-distributed-lock-examplespring
本例使用spring boot提供的redis實現,並無直接引入jedis依賴。這樣作的好處是,能夠在項目中同時使用Jedis和RedisTemplate實例。數據庫
pom.xml文件小程序
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency>
application.properties文件緩存
#redis spring.redis.database=0 spring.redis.host=18.6.8.22 spring.redis.password=Mfqy_redis_password_ spring.redis.port=6379 #鏈接超時時間(項目或鏈接池連接redis超時的時間) spring.redis.timeout=2000 #最大鏈接數(建議爲 業務指望QPS/單個鏈接的QPS,50000/1000=50) spring.redis.pool.max-active=50 #最大空閒連接數(爲減少伸縮產生的性能消耗,建議和最大鏈接數設成一致的) spring.redis.pool.max-idle=50 #最小空閒鏈接數(0表明在無請求的情況下從不建立連接) spring.redis.pool.min-idle=0 #鏈接池佔滿後沒法獲取鏈接時的阻塞時間(超時後拋出異常) spring.redis.pool.max-wait=3000
因爲咱們使用了spring boot提供的redis實現,因此咱們不能直接獲取到jedis對象。Jedis工廠類從RedisConnectionFactory中獲取Redis鏈接(JedisConnection實現類),而後使用反射的方法從中取得了Jedis實例。服務器
/** * Jedis工廠類(單例模式) */ @Service public class JedisFactory { @Autowired private RedisConnectionFactory connectionFactory; private JedisFactory(){} private static Jedis jedis; /** * 得到jedis對象 */ public Jedis getJedis() { //從RedisConnectionFactory中獲取Redis鏈接(JedisConnection實現類),而後使用反射的方法從中取得了Jedis實例 if(jedis == null){ Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis"); ReflectionUtils.makeAccessible(jedisField); jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection()); } return jedis; } }
爲避免死鎖的發生,加鎖和設定失效時間必須是一個原子性操做。不然一旦在加鎖後程序出錯,沒可以執行設置失效時間的方法時,就會產生死鎖。 可是RedisTemplate屏蔽了插入數據和設置失效時間同時執行的方法,咱們只能獲取到Jedis實例來執行。
分佈式鎖主要實現了兩個方法即佔用鎖和釋放鎖。
這裏須要注意佔用鎖和釋放鎖都要保證原子性操做,避免程序異常時產生死鎖。
鎖id主要用於標識持有鎖的請求,在釋放瑣時用來判斷只有持有正確鎖id的請求才能執行解鎖操做。
/** * redis分佈式鎖 */ @Service public class DistributedLock { @Autowired private JedisFactory JedisFactory; /** * 佔用鎖 * @param lockKey 鎖key * @return 鎖id */ public String occupyDistributedLock(String lockKey) { //得到jedis實例 Jedis jedis = JedisFactory.getJedis(); //鎖id(必須擁有此id才能釋放鎖) String lockId = UUID.randomUUID().toString(); //佔用鎖同時設置失效時間 String isSuccees = jedis.set(lockKey, lockId, "NX","PX", 15000); //佔用鎖成功返回鎖id,不然返回null if("OK".equals(isSuccees)){ return lockId; }else{ return null; } } /** * 釋放鎖 * @param lockKey 鎖key * @param lockId 加鎖id */ public void releaseDistributedLock(String lockKey,String lockId) { if(lockId != null){ //得到jedis實例 Jedis jedis = JedisFactory.getJedis(); //執行Lua代碼刪除lockId匹配的鎖 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockId)); } } }
解釋一下jedis.set(lockKey, lockId, "NX","PX", 15000)方法
格式 - String set(String key, String value, String nxxx, String expx, long time); |
Controller
/** * 分佈式鎖測試類 */ @Controller public class DistributedLockController { @Autowired private DistributedLock distributedLock; @RequestMapping(value = "/", method = RequestMethod.GET) public String index(){ return "/index"; } @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET) public String occupyDistributedLock(RedirectAttributes redirectAttributes, HttpServletRequest request){ String key = "userid:55689"; String lockId = null; try{ //佔用鎖 lockId = distributedLock.occupyDistributedLock(key); if(lockId != null){ //程序執行 TimeUnit.SECONDS.sleep(10); } }catch (Exception e){ e.printStackTrace(); }finally { //釋放鎖 distributedLock.releaseDistributedLock(key,lockId); } redirectAttributes.addFlashAttribute("lockId",lockId); return "redirect:/"; } }
頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>distributed lock</title> </head> <body> <h1>分佈式鎖測試</h1> <button onclick="window.location.href = '/occupyDistributedLock'">佔用鎖</button> <br/> <!-- 佔用成功返回鎖id --> <#if lockId??>${lockId}</#if> </body> </html>
使用Redisson提供的分佈式鎖更加方便,並且鎖的具體細節也不須要考慮。
例子已上傳碼雲:https://gitee.com/imlichao/redisson-distributed-lock-example
官網:https://redisson.org/
文檔:https://github.com/redisson/redisson/wiki
添加依賴
spring boot 中引用專用依賴,會自動生成配置和spring bean的實例。
pom.xml文件
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.10.1</version> </dependency>
配置文件
application.properties文件
#redisson spring.redis.database=0 spring.redis.host=13.6.8.1 spring.redis.password=Mfqy_redis spring.redis.port=6379
測試代碼
Controller
/** * 分佈式鎖測試類 */ @Controller public class DistributedLockController { @Autowired private RedissonClient redisson ; @RequestMapping(value = "/", method = RequestMethod.GET) public String index(){ return "/index"; } @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET) public String occupyDistributedLock(RedirectAttributes redirectAttributes){ RLock lock = null; try{ //鎖的key String key = "MF:DISTRIBUTEDLOCK:S:personId_1001"; //得到分佈式鎖實例 lock = redisson.getLock(key); //加鎖而且設置自動失效時間15秒 lock.lock(15, TimeUnit.SECONDS); //程序執行 TimeUnit.SECONDS.sleep(10); //獲取網絡時間(多服務器測試統一時間) URL url=new URL("http://www.baidu.com"); URLConnection conn=url.openConnection(); conn.connect(); long dateL=conn.getDate(); Date date=new Date(dateL); //打印和返回結果 System.out.println(date); redirectAttributes.addFlashAttribute("success",date); }catch (Exception e){ e.printStackTrace(); }finally { //釋放鎖 if (lock != null) lock.unlock(); } return "redirect:/"; } }
頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>distributed lock</title> </head> <body> <h1>分佈式鎖測試</h1> <button onclick="window.location.href = '/occupyDistributedLock'">佔用鎖</button> <br/> <#if success??>${success?datetime}</#if> </body> </html>
添加依賴
pom.xml文件
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.10.1</version> </dependency>
配置文件
application.properties文件
#redisson spring.redis.database=0 spring.redis.host=13.6.8.1 spring.redis.password=Mfqy_redis spring.redis.port=6379
配置類
/** * Redisson配置 */ @Configuration public class RedissonConfig { @Value("${spring.redis.database}") private int database; @Value("${spring.redis.host}") private String host; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.port}") private String port; @Bean RedissonClient createConfig() { Config config = new Config(); //設置編碼方式爲 Jackson JSON 編碼(不設置默認也是這個) config.setCodec(new JsonJacksonCodec()); //雲託管模式設置(咱們公司用的阿里雲redis產品) config.useReplicatedServers() //節點地址設置 .addNodeAddress("redis://"+host+":"+port) //密碼 .setPassword(password) //數據庫編號(默認0) .setDatabase(database); RedissonClient redisson = Redisson.create(config); return redisson; } }
測試代碼與SpringBoot配置同樣