如何用redis来实现分布式锁
本篇内容主要讲解“如何用redis来实现分布式锁”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何用redis来实现分布式锁”吧!
创新互联建站从2013年创立,先为吉安等服务建站,吉安等地企业,进行企业商务咨询服务。为吉安企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。
一、建Module
boot_redis01
boot_redis02
二、改POM
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
三、写YML
server.port=1111 spring.redis.database=0 spring.redis.host=localhost spring.redis.port=6379 #连接池最大连接数(使用负值表示没有限制)默认8 spring.redis.lettuce.pool.max-active=8 #连接池最大阻塞等待时间(使用负值表示没有限制)默认-1 spring.redis.lettuce.pool.max-wait=-1 #连接池中的最大空闲连接默认8 spring.redis.lettuce.pool.max-idle=8 #连接池中的最小空闲连接默认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); } }
五、业务类
1、RedisConfig配置类
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); } }
2、GoodController.java
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加过期时间是因为如果redis客户端宕机了会造成死锁,其它客户端永远获取不到锁 //2、这里将setnx与锁过期两条命令合二为一,是为了解决命令分开执行引发的原子性问题: //setnx 中间会被其它redis客户端命令加塞 2、expire //3①、为了避免线程执行业务时间大于锁过期时间导致窜行操作,再释放锁时应判断是否是自己加的锁; //还有另外一种解决方案:锁续期——额外开启一个守护线程定时给当前key加超时时间(如5s到期,每2.5s ttl判断一次,并加2.5s超时时间,不断续期,线程将使用主动删除key命令的方式释放锁;另,当此redis客户端命令宕机后,此守护线程会自动随之消亡,不会再主动续期——此机制使得其它redis客户端可以获得锁,不会发生死锁或长期等待) 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("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort); return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort; } System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort); return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort; } finally { // if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equals(value)){ // stringRedisTemplate.delete(REDIS_LOCK); // } //3②这里也存在命令的原子问题:获取当前key经相等判断后与删除对应key是两个不同命令,中间会被加塞 //解决方法1:redis事务 // stringRedisTemplate.watch(REDIS_LOCK); // while(true){ // if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){ // stringRedisTemplate.setEnableTransactionSupport(true); // stringRedisTemplate.multi(); // stringRedisTemplate.delete(REDIS_LOCK); // // List
六、改造中的问题
1、单机版没加锁
问题:没有加锁,并发下数字不对,会出现超卖现象
① synchronized 不见不散
② ReentrantLock 过时不候
在单机环境下,可以使用synchronized或Lock来实现。 但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现,比如redis或者zookeeper来构建; 不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
2、使用Nginx配置负载均衡
注:分布式部署后,单机锁还是出现超卖现象,需要分布式锁
启动两个微服务1111和2222,访问使用:http://localhost/buy_goods(即通过nginx轮询方式访问1111和2222两个微服务)
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{#反向代理的服务器列表,权重相同,即负载均衡使用轮训策略 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; # } #} }
3、异常将导致锁不会释放
① 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁
② 加锁解锁,lock/unlock必须同时出现并保证调用
4、宕机
① 部署了微服务jar包的机器挂了,代码层面根本没有走到finally这块, 没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
② 需要对lockKey有过期时间的设定
5、设置key+过期时间分开
① 设置key+过期时间分开了,必须要合并成一行具备原子性
6、张冠李戴,删除了别人的锁
① 设置锁失效时间不合理
7、finally块的判断+del删除操作不是原子性的
① 用redis自身的事务
i 未使用watch前:
ii使用watch后:
② 用Lua脚本
Redis可以通过eval命令保证代码执行的原子性
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(); } }
8、仍然存在的问题(redisson得以解决)
① Redis分布式锁如何续期? 确保redisLock过期时间大于业务执行时间的问题(锁续期)
② redis单点故障——redis异步复制造成的锁丢失, 比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。(zk/cp、redis/ap)(redis集群)
确保redisLock过期时间大于业务执行时间的问题;redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现
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("你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort); return "你已经成功秒杀商品,此时还剩余:" + realNumber + "件"+"\t 服务器端口: "+serverPort; } System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort); return "商品已经售罄/活动结束/调用超时,欢迎下次光临"+"\t 服务器端口: "+serverPort; } finally { //IllegalMonitorStateException:attempt unlock lock,not locked by current thread by node_id if(lock.isLocked() && lock.isHeldByCurrentThread()){ lock.unlock(); } } } }
注:在并发多的时候就可能会遇到这种错误,可能会被重新抢占
到此,相信大家对“如何用redis来实现分布式锁”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
本文名称:如何用redis来实现分布式锁
URL网址:http://pwwzsj.com/article/pcjiph.html