使用原生Redis完成分布式锁

使用原生Redis完成分布式锁

假设我们需要对redis中对商品库存进行减少,但是redis中可能会不存在此商品信息,此时我们就需要从数据库中取出库存将其放入redis。我们要对这个操作进行添加分布式锁。

首先,先理清业务的流程:

  1. 检查redis中是否存有商品数据,如果没有就开始获取锁。
  2. 自旋来获取锁,获取到锁之后判断是否已经有人完成添加此商品到redis的操作如果已经完成就退出,否则就进行获取数据并添加到redis中。
  3. 释放锁。

整个业务最重要的就是如何获取锁和释放锁,要保证整个过程不会出现任何的并发问题(两个线程拿到同一个商品的锁之类的)。

redis实现分布式锁和synchronized的操作相似

问题
如果有线程获取到锁,但是在执行业务的时候报错了这个锁就不会被释放怎么办?

解决方法:在向redis中添加数据的时候,给数据添加一个时间,时间一到立马删除,但是这两个操作必须是一起执行的,所以需要使用lua脚本来保证原子性。

在释放锁的过程中释放了别的其他线程的锁?

线程1获取到锁之后,在执行业务的时候时间太长,导致redis自动消除了数据完成了锁的释放,然后线程2获取到锁开始执行业务,这时线程1执行完成开始释放锁,这个时候会导致线程1释放了线程2的锁,然后线程2释放线程3······。

解决方法:在添加数据的时候给这个数据添上自己的标记,在释放的时候检查标记是否当前线程的标记,因为在删除数据的时候需要先检查数据有没有被打上标记,所以需要使用lua脚本来保证操作的原子性。

如何保证一定会执行释放锁的操作?

使用try-finally,把释放的操作放在finally代码块中。

获取锁

先查看redis中是否存有对应的商品ID,如果没有对应的id,就可以使用redis的set命令对redis中添加数据,添加数据的键为对应商品的ID,值为获取的雪花ID。生成成功就放回雪花ID。

删除锁

先获取redis中商品id对应的值,如果值和拥有的值一样就可以进行删除操作(使用lua保证原子性)。

分布锁类
package com.example.demo.utils;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;

public class RedisLock {
    private RedisTemplate<String,Object> redisTemplate;
    private final String CHECK_LOCK="	local lock=redis.call('get',KEYS[1]) " +
            "if lock~=false " +
            "then " +
            "return false " +
            "else " +
            "redis.call('set',KEYS[1],ARGV[1],'EX',ARGV[2]) " +
            "end " +
            "return true ";
    private final String DEL_LOCK="	local lock=redis.call('get',KEYS[1]) " +
            "if lock~=ARGV[1] " +
            "then " +
            "return false " +
            "else " +
            "redis.call('del',KEYS[1]) " +
            "end " +
            "return true";

    public  RedisLock(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public String getLock(String name, Long time, Long timeout){
        Long startTime=System.currentTimeMillis();
        String token = null;
        do {
            if((System.currentTimeMillis()-startTime)>timeout){
                break;
            }else{
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace(System.out);
                    return null;
                }
            }
            token=tryGetToken(name,time);
        }while (token==null);
        return token;
    }
    private String tryGetToken(String name,Long time){
        String id= String.valueOf(SnowFlake.ToGetAll());
        RedisScript<Boolean> redisScript=new DefaultRedisScript<>(CHECK_LOCK,Boolean.class);
        if(Boolean.TRUE.equals(redisTemplate.execute(redisScript, Collections.singletonList(name), id, time))){
            return id;
        }else{
            return null;
        }
    }

    public void delLock(String name,String token){
        RedisScript<Boolean> redisScript=new DefaultRedisScript<>(DEL_LOCK,Boolean.class);
        redisTemplate.execute(redisScript, Collections.singletonList(name),token);
    }
}

Integer stock= (Integer) redisTemplate.opsForHash().get("dishes",String.valueOf(shopping.getDishesId()));
                if(stock==null) {
                    //开始获取redis锁
                    RedisLock redisLock = new RedisLock(redisTemplate);
                    String token = null;
                    try {
                        do {
                            token = redisLock.getLock(String.valueOf(shopping.getDishesId()), (long) (20000), (long) (2100));
                            //检查redis中是否存有数据(是否有线程完成了此操作)
                            stock= (Integer) redisTemplate.opsForHash().get("dishes",String.valueOf(shopping.getDishesId()));
                            if(stock!=null){
                                break;
                            }
                            if(token!=null){
                                //再次判断
                                stock= (Integer) redisTemplate.opsForHash().get("dishes",String.valueOf(shopping.getDishesId()));
                                if(stock!=null){
                                    break;
                                }
                                System.out.println("获取到锁并且开始运行业务");
                                Integer dishesStock = dishesMapper.selectStocksById(shopping.getDishesId());
                                redisTemplate.opsForHash().put("dishes",String.valueOf(shopping.getDishesId()),dishesStock);
                            }
                        } while (token == null);
                    } finally {
                        //释放锁
                        redisLock.delLock(String.valueOf(shopping.getDishesId()), token);
                    }
                }
相关推荐
东方巴黎~Sunsiny7 分钟前
当kafka消费的数据滞后1000条时,打印告警信息
分布式·kafka·linq
看山还是山,看水还是。8 分钟前
Oracle的字符串函数
运维·数据库·安全·oracle
东方巴黎~Sunsiny29 分钟前
⚙️ 如何调整重试策略以适应不同的业务需求?
java·数据库·kafka
sj116373940330 分钟前
Kafka新节点加入集群操作指南
分布式·kafka
东方巴黎~Sunsiny31 分钟前
kafka消费数据太慢了,给优化下
分布式·kafka·linq
daopuyun36 分钟前
全面介绍软件安全测试分类,安全测试方法、安全防护技术、安全测试流程
网络·数据库·安全·安全测试
綦枫Maple1 小时前
Jmeter基础篇(23)TPS和QPS的异同
数据库·jmeter·oracle
ac.char1 小时前
在 Ubuntu 上安装 Redis 并为其设置登录密码
linux·redis·ubuntu
川石课堂软件测试1 小时前
性能测试|JMeter接口与性能测试项目
javascript·数据库·python·jmeter·单元测试
oneslide1 小时前
达梦数据库迁移j脚本
数据库