Redis实战(黑马点评)——分布式锁

分布式锁介绍

分布式锁:满足分布式系统或者集群环境下多进程可见并且互斥的锁。

特性MySQLRedisZookeeper
互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放

分布式锁初级版本

设计锁对象

public interface ILock {  

    /**  
     * 尝试获取锁  
     * @param timeoutSec 锁持有的超时时间,过期后自动释放  
     * @return true代表获取锁成功;false代表获取锁失败  
     */  
    boolean tryLock(long timeoutSec);  

    /**  
     * 释放锁  
     */  
    void unlock();  
}  


public class SimpleRedisLock implements ILock {
    private String name;
    private RedisTemplate redisTemplate;

    public SimpleRedisLock(String name, RedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override  
    public boolean tryLock(long timeoutSec) {
        Long ThreadId = BaseContext.getCurrent().getId();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX+ name, ThreadId+"", timeoutSec, TimeUnit.SECONDS);
//        如果 flag 为 true,返回 true;如果 flag 为 false 或 null,返回 false。
        return BooleanUtil.isTrue(flag);

    }

    @Override  
    public void unlock() {
        redisTemplate.delete(KEY_PREFIX+ name);
    }  
}  

使用锁解决一个用户的并发抢券操作

Long userId = BaseContext.getCurrent().getId();

        /**
         *          每一个请求过来,这个id对象都是一个全新的id对象,因为要是对userId加锁的话,对象变了锁就变了,那不行
         *          我们希望id的值一样,所以用了toString(),但是toString()依旧不能保证是对对象的值加锁的
         *          toString底层是new 一个String数组,还是new了一个新对象,同一个用户id在不同的请求中过来,每次都new一个,还是不能把锁加载同一个用户上
         *          于是用intern() ,intern()方法可以去字符串常量池中找字符串值一样的引用返回
         *          这样一来,如果你的userId是5,不管你new了多少个字符串,只要值是一样的,返回的结果也一样。这样就可以锁住同一个用户
         *          不同的用户不会被锁住
         */
        SimpleRedisLock lock = new SimpleRedisLock("order:"+userId, redisTemplate);
        boolean isLock = lock.tryLock(1200); // 获取锁
        if (!isLock) {
            // 获取锁失败, 返回或者重试
            return Result.fail("不允许重复下单");
        }
        try{
            // 获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();  // 拿到当前对象的代理对象,其实就是IVoucherOrderService这个接口的代理对象,返回的是Object,做个强转
            return proxy.createVoucherOrder(voucherId);  // 如果报错了是因为我们的接口中没有这个方法,那我们就在接口中创建一下这个方法就行
        }finally {
            lock.unlock();// 释放锁
        }

存在问题: 

解决方法:释放锁的时候看是不是自己获取的锁 

 解决释放的锁不是自身原先获取的锁

  1. 在获取锁时显示线程序示(可以用UUID表示)
  2. 在释放锁时先获取锁中的线程序示,判断是否与当前线程序示一致
    • 如果一致则释放锁
    • 如果不一致则不释放锁

锁对象里再设置,添加释放锁的身份确认

private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中的标识
        String id = (String) redisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标识是否一致
        if (threadId.equals(id)) {
            // 释放锁
            redisTemplate.delete(KEY_PREFIX + name);
        }
    }

Redis的Lua脚本

为什么使用 Lua 脚本?

  • 原子性:Lua 脚本在 Redis 中执行时是原子的,即脚本在执行期间不会被其他命令中断。

  • 减少网络开销:将多个操作合并为一个脚本,减少客户端与服务器之间的通信次数。

  • 复杂操作:Lua 脚本支持条件判断、循环等复杂逻辑,适合处理需要多个步骤的操作。

基本用法

  • EVAL:执行 Lua 脚本。

    EVAL "return 'Hello, Redis!'" 0

    其中,"return 'Hello, Redis!'" 是 Lua 脚本,0 表示没有键参数。

  • SCRIPT LOAD:加载脚本到 Redis,返回 SHA1 校验和。

    SCRIPT LOAD "return 'Hello, Redis!'"

    返回的 SHA1 值可用于后续的 EVALSHA 命令。

  • EVALSHA:通过 SHA1 值执行已加载的脚本。

    EVALSHA <SHA1> 0

3. 脚本中的 Redis 命令

在 Lua 脚本中,可以通过 redis.call() 或 redis.pcall() 调用 Redis 命令。

  • redis.call():执行命令,出错时抛出异常。

  • redis.pcall():执行命令,出错时返回错误信息。

EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
  • KEYS[1] 是键参数,ARGV[1] 是值参数。

  • 1 表示有 1 个键参数。

4. 脚本的原子性

Lua 脚本在执行期间,Redis 不会处理其他命令,确保脚本的原子性。但长时间运行的脚本可能导致 Redis 阻塞,需谨慎使用。

5. 脚本的缓存

Redis 会缓存加载的脚本,通过 SCRIPT LOAD 加载的脚本会一直保留,直到服务器重启或使用 SCRIPT FLUSH 清除。

6. 错误处理

  • 使用 redis.pcall() 捕获错误。

  • 脚本中的语法错误会在加载时检测到,运行时错误则会在执行时抛出。

 Lua脚本实现释放分布式锁

-- 这里的 KEYS[1] 就是传入的key,这里的 ARGV[1] 就是当前传递的参数值  
-- 获取当前的值,判断是否与当前传递的参数一致  
if (redis.call('GET', KEYS[1]) == ARGV[1]) then  
    -- 一致,则删除  
    return redis.call('DEL', KEYS[1])  
end  
-- 不一致,则返回0  
return 0  

分布式锁Redisson

Redisson 是一个用于 Java 的 Redis 客户端,它不仅提供了对 Redis 数据库的简单 API 接口,还提供了许多高级功能,旨在简化分布式应用程序的开发。它又以下的特性:

  1. 丰富的数据结构:提供了多种高级数据结构,如映射、集合、列表等,兼容 Java 集合框架。

  2. 分布式执行:支持分布式任务处理,实现高并发的任务执行。

  3. 分布式锁:确保在分布式环境下对共享资源的安全访问。

  4. 对象映射:自动序列化和反序列化 Java 对象,简化数据存取。

  5. 反应式编程支持:适合高并发和低延迟的应用程序。

  6. 高可用性:支持 Redis Sentinel 和 Redis Cluster,确保稳定运行。

使用步骤

导入依赖

<dependency>  
    <groupId>org.redisson</groupId>  
    <artifactId>redisson</artifactId>  
    <version>3.13.6</version>  
</dependency>  

配置redisson的配置类

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://localhost:6379"); // 使用正确的地址格式
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

使用Redisson

@Resource  
private RedissonClient redissonClient;  

@Test  
void testRedisson() throws InterruptedException {  
    // 获取锁(可重入),指定锁的名称  
    RLock lock = redissonClient.getLock("anyLock");  
    // 尝试获取锁,参数分别是:获取锁的最大等待时间(单位是时间尝试),锁自动释放的时间,时间单位  
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); // 无参的话就是不等待,30秒自动释放  
    // 判断释放锁获取成功  
    if (isLock) {  
        try {  
            System.out.println("执行业务");  
        } finally {  
            // 释放锁  
            lock.unlock();  
        }  
    }  
}  

Redis消息队列实现异步秒杀

消息队列(Message Queue),字面意思是存放消息的队列。最简单的消息队列模型包括三个角色:

  • 消息队列:存储和管理消息,也被称为消息中介(Message Broker)
  • 生产者:发送消息到消息队列
  • 消费者:从消息队列获取消息并处理消息

Redis 提供了三种不同的方式来实现消息队列:

  • list结构:基于 List 结构的消息队列
  • PubSub:基本的点对点消息模型
  • Stream:相对完善的消息队列模型
### 黑马点评与谷粒商城概述 黑马点评是一个基于Redis实战项目,主要讲解了如何使用Redis实现分布式锁、缓存、消息队列等功能[^1]。该项目通过理论结合实践的方式,帮助学习者深入理解Redis在实际项目中的应用,同时提升开发者的项目经验。 谷粒商城是一个综合性的电商系统项目,通常作为Java开发者进阶项目的代表[^2]。它涵盖了从用户管理、商品展示、购物车、订单处理到支付结算等完整的电商功能模块。此项目不仅帮助学习者掌握Spring Cloud微服务架构的设计与实现,还能够深入理解分布式系统的设计原理与技术细节。 --- ### 技术实现细节 #### 黑马点评 - **Redis应用**:项目中详细讲解了Redis的核心功能,如缓存设计、分布式锁、消息队列等[^1]。通过这些功能的学习,开发者可以掌握Redis在高并发场景下的最佳实践。 - **项目结构**:黑马点评的代码结构清晰,分为多个模块,包括用户模块、订单模块和评论模块等。每个模块都独立封装,便于学习者理解并扩展。 - **实战案例**:项目中模拟了一个真实的点评网站,涉及用户登录、发布评论、点赞等功能。这些功能的实现依赖于Redis的高效存储与查询能力。 #### 谷粒商城 - **微服务架构**:谷粒商城采用Spring Cloud框架构建,包含多个微服务模块,如用户服务、订单服务、商品服务等[^2]。每个模块都通过API网关进行统一管理,确保系统的可扩展性与高可用性。 - **分布式事务**:项目中实现了分布式事务解决方案,例如通过Seata或TCC模式解决跨服务的数据一致性问题。 - **性能优化**:谷粒商城对数据库、缓存、消息队列等进行了深度优化,确保系统在高并发场景下的稳定运行。例如,使用Redis缓存热门商品数据,减少数据库的压力。 - **支付集成**:项目中集成了第三方支付平台(如支付宝、微信支付),并通过异步消息队列处理支付回调,保证订单状态的及时更新。 --- ### 课程与教程推荐 #### 黑马点评 - **视频课程**:黑马程序员提供了详细的Redis视频教程,涵盖基础概念与高级应用[^1]。学习者可以通过观看视频快速上手Redis,并结合黑马点评项目进行实战练习。 - **配套文档**:课程附带了完整的项目源码与文档,帮助学习者更好地理解项目的设计思路与实现细节。 #### 谷粒商城 - **尚硅谷教程**:尚硅谷的谷粒商城课程是目前较为全面的Java进阶项目教程之一[^2]。课程内容覆盖了从需求分析到系统上线的完整流程,适合希望深入学习微服务架构的学习者。 - **开源代码**:谷粒商城的源码可以在GitHub上找到,学习者可以下载代码并根据教程逐步调试与修改,加深对微服务架构的理解。 --- ### 示例代码 以下为谷粒商城中一个简单的订单创建接口示例: ```java @RestController @RequestMapping("/orders") public class OrderController { @Autowired private OrderService orderService; @PostMapping public ResponseEntity<String> createOrder(@RequestBody OrderDto orderDto) { try { String orderId = orderService.createOrder(orderDto); return ResponseEntity.ok("Order created successfully with ID: " + orderId); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to create order: " + e.getMessage()); } } } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值