使用原生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);
                    }
                }
相关推荐
知行合一。。。2 小时前
RAG--03--Milvus基本用法
数据库·oracle·milvus
TDengine (老段)3 小时前
TDengine 线程模型 — 网络、调度、执行
大数据·数据库·物联网·制造·时序数据库·tdengine·涛思数据
上学的小垃圾4 小时前
01-数据库系统概述
数据库
二进制漫游记5 小时前
FastAPI项目集成 Qdrant向量数据库+阿里云Embedding完整实战(工具封装+业务调用)
数据库·python·阿里云·embedding
2501_928996225 小时前
GPT-4o换DeepSeek迁移成本多少?中科热备解析API聚合平台技术账本
前端·数据库·人工智能
莫得感情 o5 小时前
Redis 05 · 持久化:RDB 与 AOF 怎么保证数据不丢
redis·缓存
curd_boy6 小时前
【Redis】Redis从缓存到AI向量平台
人工智能·redis·缓存
泡干脆面就番茄7 小时前
MySQL_子查询_分页查询与联合查询详解
数据库·mysql
莫得感情 o7 小时前
Redis 02 · 数据结构:String 到 Stream 怎么选
redis
爱浦路 IPLOOK8 小时前
矿山无人化作业5G专网方案:企业专网核心网络选型分析
网络·分布式·科技·5g·信息与通信