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]
相关推荐
Aileen_0v03 分钟前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
Hello.Reader4 分钟前
Redis大Key问题全解析
数据库·redis·bootstrap
桂月二二2 小时前
Java与容器化:如何使用Docker和Kubernetes优化Java应用的部署
java·docker·kubernetes
liuxin334455662 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
小马爱打代码2 小时前
设计模式详解(建造者模式)
java·设计模式·建造者模式
B1nna3 小时前
Redis学习(三)缓存
redis·学习·缓存
栗子~~3 小时前
idea 8年使用整理
java·ide·intellij-idea
2301_801483693 小时前
Maven核心概念
java·maven
Q_19284999063 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
我要学编程(ಥ_ಥ)4 小时前
初始JavaEE篇 —— 网络原理---传输层协议:深入理解UDP/TCP
java·网络·tcp/ip·udp·java-ee