大厂五剑客之redis实战分布式缓存彻底解决方案分布式锁---06基础讲解--二期二周目

15 篇文章 0 订阅

redis实战主要是以实战为主。

简介:分布式锁是BATJ最常见的Redis面试题,对分布式锁的掌握,多场景下不同版本分布锁的掌握显得尤为关键

分布式锁是什么 版本2.6.3和2.6.4是有区别的。

  • 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现
  • 如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。

             

  • 举例redis缓存的数据来自DB,DB数据库刷缓存数据需要定时任务进行数据同步,同步的时候只可以有一台机器访问DB。

  • 分布锁设计目的

​            可以保证在分布式部署的应用集群中,同一个方法在同一操作只能被一台机器上的一个线程执行。

  • 设计要求

    • 这把锁要是一把可重入锁(避免死锁)
    • 这把锁有高可用的获取锁和释放锁功能,设置过期时间去释放锁。
    • 这把锁获取锁和释放锁的性能要好… 
  • 分布锁实现方案分析 

    • 获取锁的时候,使用 (String的特性)setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;否则返回0。setnx job "findwork"
    • 若 key 存在,则什么都不做,返回0加锁,锁的 value 值为当前占有锁服务器内网IP编号拼接任务标识
    • 在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁。 
    • 返回1则成功获取锁。还设置一个获取的超时时间, 若超过这个时间则放弃获取锁。setex(key,value,expire)过期以秒为单位
    • 释放锁的时候,判断是不是该锁(即Value为当前服务器内网IP编号拼接任务标识),若是该锁,则执行 delete 进行锁释放 删除key value,或者将过期时间设置为0。

--------------------------------------------------------------------------2-1----------------------------------------------------------------------

          

             搭建基于springboot的定时任务。

             cron表达式:

            

代码:

第一步:在启动类上开启定时任务

@SpringBootApplication
@EnableAutoConfiguration
@EnableScheduling
public class XdclassMobileRedisApplication {

    public static void main(String[] args) {
        SpringApplication.run(XdclassMobileRedisApplication.class, args);
    }

}

第二步:

-------------------------------------------------------2-2---------------------------------------------------------------------

         

       日志  springboot整合logback

       第一步:拷贝

       

       第二步:配置路径

       

      第三步:修改第一步的xml

      

       第四步:启动查看日志,这样配置的话日志是在根目录的。

      

       nohup java -jar XXX &启动后可以挂载后台的。

        

        把这三个放在服务器启动起来。

        

        需要改的端口:

                 三个文件:application.properties application.yml logback-spring.xml    

-----------------------------------------------------------------------------2-3------------------------------------------------

    

客户端

 

--------------------------------tcp--------------------------------------------2-4-------------------------------------------------

四次挥手:用来断开连接的

       

      

    

查看tcp状态

--------------------------------tcp--------------------------------------------2-5-------------------------------------------------

    

redis锁一定要设置超时时间。

 上面图的代码,分布式锁的简单代码。

代码:

 @Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {
        String lock = LOCK_PREFIX + "LockNxExJob";
        boolean nxRet = false;
        try{

            //redistemplate setnx操作 字符串
            //nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
            nxRet = redisTemplate.opsForValue().setIfAbsent(lock,"8081:3306");
            //获得这个锁
            //Object lockValue = redisService.getValue(lock);

            //获取锁失败 存在了锁
            if(!nxRet){
                String value = (String)redisService.getValue(lock);
                //打印当前占用锁的服务器IP
                logger.info("get lock fail,lock belong to:{}",value);
                return;
            }else{
                //给他超时时间
                //redisTemplate.opsForValue().set(lock,getHostIp(),3600);
                redisTemplate.opsForValue().set(lock,"8081",3600, TimeUnit.SECONDS);
                //获取锁成功
                logger.info("start lock lockNxExJob success");
                // 获得锁之后的耗时操作
                Thread.sleep(5000);
            }
        }catch (Exception e){
            logger.error("lock error",e);
        }finally {
            if(nxRet){
                logger.info("release lock success");
                // 保证只有自己加的锁才可以解锁的
                redisService.remove(lock);
            }
        }
    }

先执行finally再执行return。

tail nohup.out看启动日志的。

启动本机服务器:

---------------------------------------------3-1----------------------------------------

原理:根据key判断是加锁还是解锁,根据value判断是谁加的锁。

分布式锁的天然缺陷

分布式锁的教程:https://www.cnblogs.com/williamjie/p/9395659.html

redis锁过期时间内没有执行完耗时操作:https://blog.csdn.net/wutengfei_java/article/details/100699538

异常:服务器宕机 或者 redis宕机

服务宕机:就是setnx和setex之间down机,封装脚本。

redis宕机:

        

lua脚本。

  场景:在释放锁的时候杀掉了进程,导致锁永远不被释放的。改进的办法就是设置超时时间,形成原子的操作,必须保证超时时间是原子的,不然可能会失败就不能解锁了。

-----------------------------------------------------------------3-2-------------------------------------------------------------------

lua脚本:2.6.0开始之后才开始使用的。实现多命令去连用,保证原子性。 

package com.xdclass.mobile.xdclassmobileredis.schedule;


import com.xdclass.mobile.xdclassmobileredis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

@Service
public class LuaDistributeLock {


    private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);

    @Autowired
    private RedisService redisService;
    @Autowired
    private RedisTemplate redisTemplate;

    private static String LOCK_PREFIX = "lua_";

    private DefaultRedisScript<Boolean> lockScript;


    @Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {

        String lock = LOCK_PREFIX + "LockNxExJob";

        boolean luaRet = false;
        try {
            // 获取锁和设置过期时间放在一起的。
            luaRet = luaExpress(lock,getHostIp());

            //获取锁失败
            if (!luaRet) {
                String value = (String) redisService.genValue(lock);
                //打印当前占用锁的服务器IP
                logger.info("lua get lock fail,lock belong to:{}", value);
                return;
            } else {
                //获取锁成功
                logger.info("lua start  lock lockNxExJob success");
                Thread.sleep(5000);
            }
        } catch (Exception e) {
            logger.error("lock error", e);

        } finally {
            if (luaRet) {
                logger.info("release lock success");
                redisService.remove(lock);
            }
        }
    }


    /**
     * 获取lua结果
     * @param key
     * @param value
     * @return
     */
    public Boolean luaExpress(String key,String value) {
        lockScript = new DefaultRedisScript<Boolean>();
        lockScript.setScriptSource(
                new ResourceScriptSource(new ClassPathResource("add.lua")));
        lockScript.setResultType(Boolean.class);
        // 封装参数
        List<Object> keyList = new ArrayList<Object>();
        keyList.add(key);
        keyList.add(value);
        Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
        return result;
    }

}

代码:

lua脚本。

官方的lua脚本的执行的流程:

lua脚本用local定义变量。keys是变量传过来的。

local lockKey = KEYS[1]

lua脚本

local lockKey = KEYS[1]
local lockValue = KEYS[2]

-- setnx info
local result_1 = redis.call('SETNX', lockKey, lockValue)
if result_1 == true
then
local result_2= redis.call('SETEX', lockKey,3600, lockValue)
return result_1
else
return result_1
end

就是将开启和设置过期时间封装了为原子了。

解决key和value的乱码问题

  @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<String,String>();
        redisTemplate.setConnectionFactory(factory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        /**Jackson序列化  json占用的内存最小 */
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        /**Jdk序列化   JdkSerializationRedisSerializer是最高效的*/
//      JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
        /**String序列化*/
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        /**将key value 进行stringRedisSerializer序列化*/
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(stringRedisSerializer);
        /**将HashKey HashValue 进行序列化*/
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

如何添加lua脚本:

第一步:加入lua脚本。

第二步:写代码

  @Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {

        String lock = LOCK_PREFIX + "LockNxExJob";

        boolean luaRet = false;
        try {
//            luaRet = luaExpress(lock,getHostIp());
            luaRet = luaExpress(lock,"8082:3306");

            //获取锁失败
            if (!luaRet) {
                String value = (String) redisService.genValue(lock);
                //打印当前占用锁的服务器IP
                logger.info("lua get lock fail,lock belong to:{}", value);
                return;
            } else {
                //获取锁成功
                logger.info("lua start  lock lockNxExJob success");
                Thread.sleep(5000);
            }
        } catch (Exception e) {
            logger.error("lock error", e);

        } finally {
            if (luaRet) {
                logger.info("release lock success");
                redisService.remove(lock);
            }
        }
    }
    public Boolean luaExpress(String key,String value) {
        lockScript = new DefaultRedisScript<Boolean>();
        lockScript.setScriptSource(
                new ResourceScriptSource(new ClassPathResource("add.lua")));
        lockScript.setResultType(Boolean.class);
        // 封装参数
        List<Object> keyList = new ArrayList<Object>();
        keyList.add(key);
        keyList.add(value);
        Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
        return result;
    }

lua脚本其他的用法:https://www.jb51.net/article/173679.htm

-------------------------------------------------------------------3-3-------------------------------------------------------------

  

  打开RedisTemplate

private @Nullable ValueOperations<K, V> valueOps;//进入这个里面
private @Nullable ListOperations<K, V> listOps;
private @Nullable SetOperations<K, V> setOps;
private @Nullable ZSetOperations<K, V> zSetOps;
private @Nullable GeoOperations<K, V> geoOps;
private @Nullable HyperLogLogOperations<K, V> hllOps;

  

  

  原理:通过redisTemplate拿到回调函数。

代码:

  public boolean setLock(String key, long expire) {
        try {
            Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.set(key.getBytes(), "锁定的资源".getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
                }
            });
            return result;
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
        }
        return false;
    }

 RedisConnention:https://www.cnblogs.com/jiawen010/articles/11358031.html

RedisStringCommands支持连用的

此处转化不成功:

这个方法和lua脚本一样也是setnx和setex一起执行。     

补充知识点:重入锁,记录进入和退出的次数。

-------------------------------------RedisConnectios实现分布式锁-----------------------------------3-4----------------------

redis基础知识:https://www.cnblogs.com/jpfss/p/8431249.html

目前有三种方案实现:

1.正常的使用有问题setna和setex之间会死锁的。

2.lua脚本原子。

3.jedis原子封装。

解锁过程中分布式锁的优化分析:

执行时间超过锁的延时时间。

同一把锁就是同一个key,但是解锁只能解自己的锁,就是用value判断是不是自己的锁。

就是锁已经释放了但是任务还没有执行完。

解决办法:判断value,是当前的锁就释放掉,不是自己的锁就不能解锁。

但是还有问题,可能拿到本地之后value改变,实际解锁。

代码:

   private boolean releaseLock(String key, String value) {
        lockScript = new DefaultRedisScript<Boolean>();
        lockScript.setScriptSource(
                new ResourceScriptSource(new ClassPathResource("unlock.lua")));
        lockScript.setResultType(Boolean.class);
        // 封装参数
        List<Object> keyList = new ArrayList<Object>();
        keyList.add(key);
        keyList.add(value);
        Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
        return result;
    }

就已经换了,这样关闭的就是新锁不是自己的锁。

满足加锁和解锁是所以关闭也要是原子的。

local lockKey = KEYS[1]
local lockValue = KEYS[2]

-- get key
local result_1 = redis.call('get', lockKey)
if result_1 == lockValue
then
local result_2= redis.call('del', lockKey)
return result_2
else
return false
end
  @Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {

        String lock = LOCK_PREFIX + "JedisNxExJob";
        boolean lockRet = false;
        try {
            lockRet = this.setLock(lock, 600);

            //获取锁失败
            if (!lockRet) {
                String value = (String) redisService.genValue(lock);
                //打印当前占用锁的服务器IP
                logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
                return;
            } else {
                //获取锁成功
                logger.info("jedisLockJob start  lock lockNxExJob success");
                Thread.sleep(5000);
            }
        } catch (Exception e) {
            logger.error("jedisLockJob lock error", e);

        } finally {
            if (lockRet) {
                logger.info("jedisLockJob release lock success");
                releaseLock(lock,getHostIp());
            }
        }
    }

------------------------------------- lua脚本高可用redis的优化-----------------------------------3-5--------------------

        

        

        

------------------------------------------------------支付宝------------------------------------3-6-------------------

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值