使用原生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);
                    }
                }
相关推荐
sdaxue.com9 分钟前
帝国CMS:如何去掉帝国CMS登录界面的认证码登录
数据库·github·网站·帝国cms·认证码
o(╥﹏╥)1 小时前
linux(ubuntu )卡死怎么强制重启
linux·数据库·ubuntu·系统安全
阿里嘎多学长1 小时前
docker怎么部署高斯数据库
运维·数据库·docker·容器
Yuan_o_1 小时前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
Sunyanhui11 小时前
牛客网 SQL36查找后排序
数据库·sql·mysql
老王笔记1 小时前
MHA binlog server
数据库·mysql
lovelin+v175030409662 小时前
安全性升级:API接口在零信任架构下的安全防护策略
大数据·数据库·人工智能·爬虫·数据分析
道一云黑板报3 小时前
Flink集群批作业实践:七析BI批作业执行
大数据·分布式·数据分析·flink·kubernetes
DT辰白3 小时前
基于Redis的网关鉴权方案与性能优化
数据库·redis·缓存