本篇內(nèi)容主要講解“如何用redis來(lái)實(shí)現(xiàn)分布式鎖”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“如何用redis來(lái)實(shí)現(xiàn)分布式鎖”吧!
創(chuàng)新互聯(lián)建站從2013年創(chuàng)立,先為吉安等服務(wù)建站,吉安等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為吉安企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。
boot_redis01
boot_redis02
4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.4 com.lau boot_redis01 0.0.1-SNAPSHOT boot_redis01 Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 redis.clients jedis 3.1.0 org.springframework.boot spring-boot-starter-aop org.redisson redisson 3.13.4 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true junit junit 4.12 org.springframework.boot spring-boot-maven-plugin
server.port=1111 spring.redis.database=0 spring.redis.host=localhost spring.redis.port=6379 #連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)默認(rèn)8 spring.redis.lettuce.pool.max-active=8 #連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)默認(rèn)-1 spring.redis.lettuce.pool.max-wait=-1 #連接池中的最大空閑連接默認(rèn)8 spring.redis.lettuce.pool.max-idle=8 #連接池中的最小空閑連接默認(rèn)0 spring.redis.lettuce.pool.min-idle=0 ?
package com.lau.boot_redis01; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) public class BootRedis01Application { public static void main(String[] args) { SpringApplication.run(BootRedis01Application.class, args); } }
package com.lau.boot_redis01.config; import org.redisson.Redisson; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.io.Serializable; @Configuration public class RedisConfig { @Value("${spring.redis.host}") private String redisHost; /** *保證不是序列化后的亂碼配置 */ @Bean public RedisTemplateredisTemplate(LettuceConnectionFactory connectionFactory){ RedisTemplate redisTemplate =new RedisTemplate<>(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } @Bean public Redisson redisson(){ Config config = new Config(); config.useSingleServer().setAddress("redis://"+redisHost+":6379").setDatabase(0); return (Redisson) Redisson.create(config); } }
package com.lau.boot_redis01.controller; import com.lau.boot_redis01.util.RedisUtil; import org.springframework.web.bind.annotation.RestController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import redis.clients.jedis.Jedis; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; @RestController public class GoodController { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String serverPort; private static final String REDIS_LOCK = "atguigulock"; @GetMapping("/buy_goods") public String buy_Goods() throws Exception { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); try{ //1、key加過(guò)期時(shí)間是因?yàn)槿绻鹯edis客戶端宕機(jī)了會(huì)造成死鎖,其它客戶端永遠(yuǎn)獲取不到鎖 //2、這里將setnx與鎖過(guò)期兩條命令合二為一,是為了解決命令分開(kāi)執(zhí)行引發(fā)的原子性問(wèn)題: //setnx 中間會(huì)被其它redis客戶端命令加塞 2、expire //3①、為了避免線程執(zhí)行業(yè)務(wù)時(shí)間大于鎖過(guò)期時(shí)間導(dǎo)致竄行操作,再釋放鎖時(shí)應(yīng)判斷是否是自己加的鎖; //還有另外一種解決方案:鎖續(xù)期——額外開(kāi)啟一個(gè)守護(hù)線程定時(shí)給當(dāng)前key加超時(shí)時(shí)間(如5s到期,每2.5s ttl判斷一次,并加2.5s超時(shí)時(shí)間,不斷續(xù)期,線程將使用主動(dòng)刪除key命令的方式釋放鎖;另,當(dāng)此redis客戶端命令宕機(jī)后,此守護(hù)線程會(huì)自動(dòng)隨之消亡,不會(huì)再主動(dòng)續(xù)期——此機(jī)制使得其它redis客戶端可以獲得鎖,不會(huì)發(fā)生死鎖或長(zhǎng)期等待) Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);//setnx if(!flag){ return "獲取鎖失??!"; } String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0){ int realNumber = goodsNumber - 1; stringRedisTemplate.opsForValue().set("goods:001",realNumber + ""); System.out.println("你已經(jīng)成功秒殺商品,此時(shí)還剩余:" + realNumber + "件"+"\t list = stringRedisTemplate.exec(); // // if(list == null){ // continue; // } // } // // stringRedisTemplate.unwatch(); // break; // } //解決方法2:lua腳本——原子操作 Jedis jedis = RedisUtil.getJedis(); String script = "if redis.call('get', KEYS[1]) == ARGV[1]"+"then " +"return redis.call('del', KEYS[1])"+"else "+ " return 0 " + "end"; try{ Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value)); if ("1".equals(result.toString())){ System.out.println("------del REDIS_LOCK_KEY success"); } else { System.out.println("------del REDIS_LOCK_KEY error"); } }finally { if (null != jedis){ jedis.close(); } } } } }
問(wèn)題:沒(méi)有加鎖,并發(fā)下數(shù)字不對(duì),會(huì)出現(xiàn)超賣現(xiàn)象
① synchronized 不見(jiàn)不散
② ReentrantLock 過(guò)時(shí)不候
在單機(jī)環(huán)境下,可以使用synchronized或Lock來(lái)實(shí)現(xiàn)。 但是在分布式系統(tǒng)中,因?yàn)楦?jìng)爭(zhēng)的線程可能不在同一個(gè)節(jié)點(diǎn)上(同一個(gè)jvm中),所以需要一個(gè)讓所有進(jìn)程都能訪問(wèn)到的鎖來(lái)實(shí)現(xiàn),比如redis或者zookeeper來(lái)構(gòu)建; 不同進(jìn)程jvm層面的鎖就不管用了,那么可以利用第三方的一個(gè)組件,來(lái)獲取鎖,未獲取到鎖,則阻塞當(dāng)前想要運(yùn)行的線程
注:分布式部署后,單機(jī)鎖還是出現(xiàn)超賣現(xiàn)象,需要分布式鎖
啟動(dòng)兩個(gè)微服務(wù)1111和2222,訪問(wèn)使用:http://localhost/buy_goods(即通過(guò)nginx輪詢方式訪問(wèn)1111和2222兩個(gè)微服務(wù))
nginx.conf配置
#user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; upstream mynginx{#反向代理的服務(wù)器列表,權(quán)重相同,即負(fù)載均衡使用輪訓(xùn)策略 server localhost:1111 weight=1; server localhost:2222 weight=1; } server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { #root html; #index index.html index.htm; proxy_pass http://mynginx;#配置反向代理 index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # location / { # root html; # index index.html index.htm; # } #} # HTTPS server # #server { # listen 443 ssl; # server_name localhost; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} }
① 出異常的話,可能無(wú)法釋放鎖, 必須要在代碼層面finally釋放鎖
② 加鎖解鎖,lock/unlock必須同時(shí)出現(xiàn)并保證調(diào)用
① 部署了微服務(wù)jar包的機(jī)器掛了,代碼層面根本沒(méi)有走到finally這塊, 沒(méi)辦法保證解鎖,這個(gè)key沒(méi)有被刪除,需要加入一個(gè)過(guò)期時(shí)間限定key
② 需要對(duì)lockKey有過(guò)期時(shí)間的設(shè)定
① 設(shè)置key+過(guò)期時(shí)間分開(kāi)了,必須要合并成一行具備原子性
① 設(shè)置鎖失效時(shí)間不合理
① 用redis自身的事務(wù)
i 未使用watch前:
ii使用watch后:
② 用Lua腳本
Redis可以通過(guò)eval命令保證代碼執(zhí)行的原子性
java配置類:
package com.lau.boot_redis01.util; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisUtil { private static JedisPool jedisPool; static { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,100000); } public static Jedis getJedis() throws Exception{ if (null!=jedisPool){ return jedisPool.getResource(); } throw new Exception("Jedispool is not ok"); } }
Jedis jedis = RedisUtil.getJedis(); String script = "if redis.call('get', KEYS[1]) == ARGV[1]"+"then " +"return redis.call('del', KEYS[1])"+"else "+ " return 0 " + "end"; try{ Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value)); if ("1".equals(result.toString())){ System.out.println("------del REDIS_LOCK_KEY success"); } else { System.out.println("------del REDIS_LOCK_KEY error"); } }finally { if (null != jedis){ jedis.close(); } }
① Redis分布式鎖如何續(xù)期? 確保redisLock過(guò)期時(shí)間大于業(yè)務(wù)執(zhí)行時(shí)間的問(wèn)題(鎖續(xù)期)
② redis單點(diǎn)故障——redis異步復(fù)制造成的鎖丟失, 比如:主節(jié)點(diǎn)沒(méi)來(lái)的及把剛剛set進(jìn)來(lái)這條數(shù)據(jù)給從節(jié)點(diǎn),就掛了。(zk/cp、redis/ap)(redis集群)
確保redisLock過(guò)期時(shí)間大于業(yè)務(wù)執(zhí)行時(shí)間的問(wèn)題;redis集群環(huán)境下,我們自己寫(xiě)的也不OK, 直接上RedLock之Redisson落地實(shí)現(xiàn)
1、RedisConfig.java
@Bean public Redisson redisson(){ Config config = new Config(); config.useSingleServer().setAddress("redis://"+redisHost+":6379").setDatabase(0); return (Redisson) Redisson.create(config); }
2、控制器類:
package com.lau.boot_redis01.controller; import com.lau.boot_redis01.util.RedisUtil; import lombok.val; import org.redisson.Redisson; import org.redisson.RedissonLock; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; @RestController public class GoodController_Redisson { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${server.port}") private String serverPort; private static final String REDIS_LOCK = "atguigulock"; @Autowired private Redisson redisson; @GetMapping("/buy_goods2") public String buy_Goods() throws Exception { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); RLock lock = redisson.getLock(REDIS_LOCK); try{ lock.lock(); String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.parseInt(result); if (goodsNumber > 0){ int realNumber = goodsNumber - 1; stringRedisTemplate.opsForValue().set("goods:001",realNumber + ""); System.out.println("你已經(jīng)成功秒殺商品,此時(shí)還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort); return "你已經(jīng)成功秒殺商品,此時(shí)還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort; } System.out.println("商品已經(jīng)售罄/活動(dòng)結(jié)束/調(diào)用超時(shí),歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort); return "商品已經(jīng)售罄/活動(dòng)結(jié)束/調(diào)用超時(shí),歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort; } finally { //IllegalMonitorStateException:attempt unlock lock,not locked by current thread by node_id if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } }
注:在并發(fā)多的時(shí)候就可能會(huì)遇到這種錯(cuò)誤,可能會(huì)被重新?lián)屨?/p>
到此,相信大家對(duì)“如何用redis來(lái)實(shí)現(xiàn)分布式鎖”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!