• 十、redis分布式锁:基础篇


    一、基础知识

    1、锁的种类

    • 单机版同一个JVM虚拟机内,synchronized 或者 Lock 接口。
    • 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

    2、分布式锁需要具备的条件和刚需

    • 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有
    • 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况
    • 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
    • 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放
    • 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁

    3、分布式锁

    多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。

    二、案例v1.1

    1、使用场景

    多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)。

    2、基础代码

    @RestController
    public class DemoController {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @Autowired
        public static final String KEY = "MY_REDIS";
    
        @GetMapping("/test")
        public void test() {
            String result = stringRedisTemplate.opsForValue().get(KEY);
            //  执行业务代码
            //  ...
        }
    }

    三、v1.2

    1、v1.1问题

    没有加锁,不具备独占性。可以使用synchronized或Lock来实现,建议使用Lock,可以自由设定超时时间,sync只能死等。

    2、升级

    @RestController
    public class DemoController {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        private final Lock lock = new ReentrantLock();
    
        @Autowired
        public static final String KEY = "MY_REDIS";
    
        @GetMapping("/test")
        public void test() {       
            if (lock.tryLock())
            {
                try
                {
                    //  执行业务代码
                    //  ...
                }finally {
                    lock.unlock();
                }
            }
        }
    }

    四、v1.3

    1、v1.2问题

    只能在单机加锁生效,无法使用在分布式系统上。需要一个让所有进程都能访问到的锁来实现。

    2、升级

    public class DemoController {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @GetMapping("/test")
        public void test() {
            String key = "MY_REDIS";
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    
            Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
    
            if (!flagLock) {
                System.out.println("抢夺锁失败");
            }
    
            //  执行业务代码
            //  ...
        }
    }

    五、v1.4

    1、v1.3问题

    只考虑了正常流程,如果出异常,可能无法释放锁,必须要在代码层面finally释放锁。

    2、升级

    @RestController
    public class DemoController {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @GetMapping("/test")
        public void test() {
            String key = "MY_REDIS";
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    
            try {
                Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
    
                if (!flagLock) {
                    System.out.println("抢夺锁失败");
                }
    
                //  执行业务代码
                //  ...
            } finally {
                stringRedisTemplate.delete(key);
            }
        }
    }

    六、v1.5

    1、v1.4问题

    如果代码没走带finally这步,比如部署了微服务Jar包的机器挂了,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key。

    2、升级

    
      Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
      stringRedisTemplate.expire(key,10L, TimeUnit.SECONDS);

    七、v1.6

    1、v1.5问题

    设置key + 过期时间分开了,必须要合并成一行具备原子性。可能代码只走到设置key的这步就挂了。

    2、升级

    //  同步设置锁和过期时间
    Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS);

    八、v1.7

    1、v1.6问题

    可能删除了别人的锁。比如:A线程执行业务代码需要15秒,锁的过期时间只有10秒,这时候B在第11秒的时候进来了。当第15秒时A执行完业务代码后,会直接删除B的锁。

    2、升级

    @RestController
    public class DemoController {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @GetMapping("/test")
        public void test() {
            String key = "MY_REDIS";
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    
            try {
                //  同步设置锁和过期时间
                Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS);
    
                if (!flagLock) {
                    System.out.println("抢夺锁失败");
                }
    
                //  执行业务代码
                //  ...
            } finally {
                //  判断是否当前线程加的锁
                if (stringRedisTemplate.opsForValue().get(key).equals(value)) {
                    stringRedisTemplate.delete(key);
                }
            }
        }
    }

    九、v1.8

    1、v1.7问题

    判断加锁与解锁是不是同一个客户端,finally块的判断+del删除操作不是原子性的,Redis调用Lua脚本通过eval命令保证代码执行的原子性。

    2、升级

    (1)RedisUtils

    public class RedisUtils
    {
        private static JedisPool jedisPool;
    
        static {
            JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(20);
            jedisPoolConfig.setMaxIdle(10);
            jedisPool=new JedisPool(jedisPoolConfig,"192.168.111.147",6379);
        }
    
        public static Jedis getJedis() throws Exception {
            if(null!=jedisPool){
                return jedisPool.getResource();
            }
            throw new Exception("Jedispool was not init");
        }
    
    }

    (2)升级代码

    @RestController
    public class DemoController {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        @GetMapping("/test")
        public void test() throws Exception {
            String key = "MY_REDIS";
            String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
    
            try {
                //  同步设置锁和过期时间
                Boolean flagLock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, 10L, TimeUnit.SECONDS);
    
                if (!flagLock) {
                    System.out.println("抢夺锁失败");
                }
    
                //  执行业务代码
                //  ...
            } finally {
                Jedis jedis = RedisUtils.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(key), 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();
                    }
                }
            }
        }
    }

    十、v1.9

    1、v1.8问题

    只能基于单个Redis节点实现分布式锁,无法使用在多个redis节点中实现。官方推荐RedLock之Redisson,Redisson中的 “看门狗” 会自动实现锁的续期

    2、升级

    (1)RedisConfig

    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
            RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
    
            redisTemplate.setConnectionFactory(lettuceConnectionFactory);
            //  设置key序列号方式String
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            //  设置value的序列化方式json
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
    
            redisTemplate.afterPropertiesSet();
    
            return redisTemplate;
        }
    
        @Bean
        public Redisson redisson() {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://192.168.1.108:6379").setDatabase(0);
            return (Redisson) Redisson.create(config);
        }
    }

    (2)升级代码

    @RestController
    public class DemoController {
        @Autowired
        private Redisson redisson;
    
        @GetMapping("/test")
        public void test() throws Exception {
            String key = "MY_REDIS";
            RLock redissonLock = redisson.getLock(key);
            redissonLock.lock();
            try {
                //  执行业务代码
                //  ...
            } finally {
                redissonLock.unlock();
            }
        }
    }

    3、完善

    高并发下会报异常

     升级

    @RestController
    public class DemoController {
        @Autowired
        private Redisson redisson;
    
        @GetMapping("/test")
        public void test() throws Exception {
            String key = "MY_REDIS";
            RLock redissonLock = redisson.getLock(key);
            redissonLock.lock();
            try {
                //  执行业务代码
                //  ...
            } finally {
                if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
                    redissonLock.unlock();
                }
            }
        }
    }
  • 相关阅读:
    ES 使用小结
    TruncateATable 清除一张表
    js 排序,去重
    读高性能JavaScript编程 第四章 Conditionals
    读高性能JavaScript编程 第四章 Duff's Device
    c# AOP 文章地址
    String、StringBuffer与StringBuilder之间区别
    批处理命令
    C#中的is和as操作符
    c# 入门
  • 原文地址:https://www.cnblogs.com/shiblog/p/15831108.html
Copyright © 2020-2023  润新知