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]
相关推荐
向上的车轮7 分钟前
基于Java Spring Boot的云原生TodoList Demo 项目,验证云原生核心特性
java·spring boot·云原生
程序员清风9 分钟前
快手一面:为什么要求用Static来修饰ThreadLocal变量?
java·后端·面试
逍遥德10 分钟前
Java8 Comparator接口 和 List Steam 排序使用案例
java·spring boot·list·排序算法
前行的小黑炭28 分钟前
Android :如何快速让布局适配手机和平板?
android·java·kotlin
酷ku的森1 小时前
Redis中的Zset数据类型
数据库·redis·缓存
_BugMan2 小时前
【IDEA】干活?一个IDEA即可,集成开发平台打造攻略
java·ide·intellij-idea
YA3332 小时前
java设计模式二、工厂
java·开发语言·设计模式
金色天际线-2 小时前
Nginx 优化与防盗链配置指南
java·后端·spring
我爱挣钱我也要早睡!3 小时前
Java 复习笔记
java·开发语言·笔记
AD钙奶-lalala5 小时前
Mac OS上搭建 http server
java