Spring Retry + Redis Watch实现高并发乐观锁

为什么不用分布式锁

分布式锁属于悲观锁,不利于并发优化

能不能用Redis+Lua

利用Redis+Lua单线程原子性特性,可以解决高并发且无锁,单对于复杂业务逻辑,例如加入数据库业务逻辑判断,Lua非常不友好,且不容易调试

java 复制代码
package com.itlaoqi.redislua;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;

@RestController
public class LuaController {
    private static final String LUA_SCRIPT = """
        if tonumber(redis.call('exists', KEYS[1])) == 0 then
            redis.call('set', KEYS[1],'10')
        end
        
        if tonumber(redis.call('exists', KEYS[2])) == 0 then
            redis.call('sadd', KEYS[2],'-1')
        end
        
        if tonumber(redis.call('get', KEYS[1])) > 0 and tonumber(redis.call('sismember', KEYS[2] , ARGV[1])) == 0  then 
            redis.call('incrby', KEYS[1],'-1') 
            redis.call('sadd',KEYS[2],ARGV[1])
            return 1
        else 
            return 0 
        end
    """;

    @Autowired
    private StringRedisTemplate redisTemplate;
    @GetMapping("/sk")
    public Map secKill(String pid){
        Map resp = new HashMap();
        String uid = String.valueOf(new Random().nextInt(100000000));
        List keys = new ArrayList();
        keys.add("P" + pid); //P1010 String类型 用于保存1010产品库存量
        keys.add("U" + pid);//U1010 SET类型 用于保存秒杀确权的UID
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LUA_SCRIPT,Long.class);
        Long result = redisTemplate.execute(redisScript, keys,uid);
        resp.put("uid", uid);
        resp.put("result", result);
        return resp;
    }
}

Spring Retry + Redis Watch实现乐观锁

通过多次重试实现最小程度锁定,开发模式利用Java语言接口

Redis中的事务是指在单个步骤中执行一组命令,围绕着MULTI、EXEC、DISCARD和WATCH命令展开。

引入依赖

java 复制代码
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <!-- <dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.23.5</version>
</dependency>-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>2.0.0</version>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
  </dependency>
</dependencies>

启用spring-retry

java 复制代码
@SpringBootApplication
@EnableRetry
public class RedisClientSideApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisClientSideApplication.class, args);
    }

}

业务逻辑

java 复制代码
package com.itwenqiang.redisclientside;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class SampleService {
    @Autowired
    private RedisTemplate redisTemplate;

    @Retryable(retryFor = IllegalStateException.class, maxAttempts = 2)
    @Transactional
    public String sa(){
        System.out.println("executing sa()");
        List execute = (List)redisTemplate.execute(new SessionCallback<List>() {
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
                redisTemplate.watch("sa001");
                redisTemplate.multi();
                redisTemplate.opsForValue().set("pri001",-100);
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                redisTemplate.opsForValue().set("sa001",100);
                return redisTemplate.exec();
            }
        });
        if(execute.size()==0){
            System.out.println("发现并发冲突:" + execute);
            throw new IllegalStateException("Retry");
        }else{
            System.out.println("exec执行成功:" + execute);
        }
        return "success";
    }
}

控制器

java 复制代码
package com.itwenqiang.redisclientside;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SampleService sampleService;

    @GetMapping("/test")
    public String testWatch(){
        sampleService.sa();
        return "success";
    }

    @GetMapping("/setSA")
    public String setSA(){
        redisTemplate.opsForValue().set("sa001",300);
        return "success";
    }

}

测试代码

java 复制代码
GET http://localhost:8080/test
GET http://localhost:8080/setSA

执行结果

java 复制代码
executing sa()
发现并发冲突:[]
executing sa()
exec执行成功:[true, true]
相关推荐
yxlalm4 小时前
Spring AI+RAG 01-项目背景与技术选型
java·人工智能·spring
程序员清风5 小时前
Java 后端高并发设计:线程池、限流、熔断与降级
java·数据库·oracle
Seoyoneh5 小时前
Agentic Workflow编排架构:云客服从“被动响应”迈向“主动执行”的技术实现
java·开发语言·架构
海南java第二人5 小时前
对外 API 如何保证幂等性?插入数据 / 上传表格场景全方案汇总
java·幂等性
tryxr6 小时前
Chat2Excel 文件服务模块上传文件功能开发
java·项目·oss·easyexcel·文件服务
Tangyuewei6 小时前
Java 写 Agent:模型只是组件
java·开发语言
彧azz7 小时前
Java学习语法篇:变量
java·学习
信誓旦旦的程序猿8 小时前
【量化系统从零构建 #04】存储设计:选型·建库·交易日历
java·人工智能·python·股票数据api·股票数据·股票数据api接口·股票api数据接口
yurenpai(27届找实习中)8 小时前
Java 10 的 var 为什么不能随便用?从订单汇总看懂类型推断与泛型陷阱
java·开发语言·java18
Flynt9 小时前
把公司项目迁到 Spring Boot 4.0:编译通过只是开始
java·spring boot·后端