seata tcc模式 使用redis幂等

package com.kongjs.mall.tcc.impl;

import com.kongjs.mall.mapper.GoodsStockMapper;
import com.kongjs.mall.tcc.model.dto.GoodsStockDTO;
import com.kongjs.mall.tcc.service.GoodsStockTccService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.seata.rm.tcc.api.BusinessActionContext;
import org.apache.seata.rm.tcc.api.LocalTCC;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RMap;
import org.redisson.api.RedissonClient;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.transaction.support.TransactionTemplate;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@LocalTCC
@DubboService
public class GoodsStockTccServiceImpl implements GoodsStockTccService {

    // 事务状态定义
    public static final int STATUS_INIT = 0;         // 初始状态
    public static final int STATUS_TRIED = 1;        // 准备阶段完成
    public static final int STATUS_COMMITTED = 2;    // 已提交
    public static final int STATUS_ROLLBACKED = 3;   // 已回滚
    public static final int STATUS_SUSPENDED = 4;    // 已挂起

    // Redis键前缀
    private static final String STOCK_AVAILABLE_KEY = "goods:stock:available:";  // 可用库存
    private static final String STOCK_FROZEN_KEY = "goods:stock:frozen:";        // 冻结库存
    private static final String STOCK_LOCK_KEY = "goods:stock:lock:";            // 分布式锁

    // 超时配置(高并发优化)
    private static final long LOCK_WAIT_TIME = 3;     // 锁等待时间缩短为3秒
    private static final long LOCK_LEASE_TIME = 10;   // 锁持有时间缩短为10秒(配合续期)
    private static final long MARK_EXPIRE_TIME = 86400;  // 状态标记过期时间(24小时)

    // Lua脚本(原子性操作库存与状态)
    private static final String PREPARE_LUA_SCRIPT;
    private static final String COMMIT_LUA_SCRIPT;
    private static final String ROLLBACK_LUA_SCRIPT;

    static {
        try {
            PREPARE_LUA_SCRIPT = new ClassPathResource("lua/goodsStockPrepare.lua").getContentAsString(Charset.defaultCharset());
            COMMIT_LUA_SCRIPT = new ClassPathResource("lua/goodsStockCommit.lua").getContentAsString(Charset.defaultCharset());
            ROLLBACK_LUA_SCRIPT = new ClassPathResource("lua/goodsStockRollback.lua").getContentAsString(Charset.defaultCharset());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private TransactionTemplate transactionTemplate;

    @Resource
    private GoodsStockMapper goodsStockMapper;

    /**
     * TCC准备阶段优化点:
     * 1. 解决幂等性:通过Redis锁+业务参数校验
     * 2. 防悬挂:设置合理的锁超时时间,确保资源预留成功才返回
     * 3. 原子操作:Redis+Lua保证库存操作原子性
     */
    @Override
    public boolean prepare(BusinessActionContext context, GoodsStockDTO dto) {
        String xid = context.getXid();
        Long goodsId = dto.getGoodsId();
        Integer reduceCount = dto.getReduceCount();
        // 1. 幂等性校验:已完成状态直接返回成功
        Integer status = getStatus(context);
        if (status != null) {
            if (status == STATUS_COMMITTED || status == STATUS_ROLLBACKED) {
                log.info("Prepare幂等处理(已完成), xid:{}, status:{}", xid, status);
                return true;
            } else if (status == STATUS_TRIED) {
                log.info("Prepare幂等处理(处理中), xid:{}", xid);
                return true; // 防止重复执行
            }
        }
        RLock lock = redissonClient.getLock(getLockKey(goodsId));
        try {
            boolean locked;
            try {
                locked = lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
            if (!locked) {
                log.warn("获取锁失败, goodsId:{}, xid:{}", goodsId, xid);
                return false;  // 不抛异常,让上层重试
            }
            transactionTemplate.executeWithoutResult((transactionStatus)->{
                try {
                    syncPrepareToDb(goodsId, reduceCount);
                    DefaultRedisScript<Long> script = new DefaultRedisScript<>(PREPARE_LUA_SCRIPT, Long.class);
                    Long result = stringRedisTemplate.execute(script, Arrays.asList(getAvailableKey(goodsId), getFrozenKey(goodsId)), reduceCount.toString());
                    if (!Objects.equals(result, 1L)) {
                        throw new RuntimeException();
                    }
                    setStatusTried(context);
                } catch (Throwable e){
                    transactionStatus.setRollbackOnly();
                }
            });
            return true;
        } finally {
            // 确保锁释放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }


    /**
     * TCC提交阶段优化点:
     * 1. 幂等处理:重复提交视为成功
     * 2. 空回滚防护:检查准备阶段标记
     * 3. 异常处理:数据库异常时抛出异常触发重试
     */
    @Override
    public boolean commit(BusinessActionContext context) {
        String xid = context.getXid();
        GoodsStockDTO dto = context.getActionContext("dto", GoodsStockDTO.class);
        Long goodsId = dto.getGoodsId();
        Integer reduceCount = dto.getReduceCount();
        Integer status = getStatus(context);
        if (status == null) {
            return true;
        }
        if (status.equals(STATUS_COMMITTED)) {
            return true;
        }
        if (status.equals(STATUS_ROLLBACKED) || status.equals(STATUS_SUSPENDED)) {
            return false;
        }
        RLock lock = redissonClient.getLock(getLockKey(goodsId));
        try {
            boolean locked;
            try {
                locked = lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
            if (!locked) {
                log.warn("提交阶段获取锁失败, goodsId:{}, xid:{}", goodsId, xid);
                return false;
            }
            transactionTemplate.executeWithoutResult((transactionStatus -> {
                try {
                    syncCommitToDb(goodsId, reduceCount);
                    String frozenKey = STOCK_FROZEN_KEY + goodsId;
                    DefaultRedisScript<Long> script = new DefaultRedisScript<>(COMMIT_LUA_SCRIPT, Long.class);
                    Long result = stringRedisTemplate.execute(script, Collections.singletonList(frozenKey), reduceCount.toString());
                    if (!Objects.equals(result, 1L)) {
                        throw new RuntimeException();
                    }
                    setStatusCommitted(context);
                }catch (Throwable e){
                    transactionStatus.setRollbackOnly();
                }
            }));
            return true;
        } finally {
            // 确保锁释放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }


    /**
     * TCC回滚阶段优化点:
     * 1. 幂等处理:重复回滚视为成功
     * 2. 空回滚防护:检查准备阶段标记
     * 3. 最终一致性:确保库存正确恢复
     */
    @Override
    public boolean rollback(BusinessActionContext context) {
        String xid = context.getXid();
        GoodsStockDTO dto = context.getActionContext("dto", GoodsStockDTO.class);
        Long goodsId = dto.getGoodsId();
        Integer reduceCount = dto.getReduceCount();
        Integer status = getStatus(context);
        if (status == null) {
            return true;
        }
        if (status.equals(STATUS_COMMITTED)) {
            return false;
        }
        if (status.equals(STATUS_ROLLBACKED) || status.equals(STATUS_SUSPENDED)) {
            return true;
        }
        RLock lock = redissonClient.getLock(getLockKey(goodsId));
        try {
            boolean locked;
            try {
                locked = lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
            if (!locked) {
                log.warn("回滚阶段获取锁失败, goodsId:{}, xid:{}", goodsId, xid);
                return false;
            }
            transactionTemplate.executeWithoutResult(transactionStatus -> {
                try {
                    syncRollbackToDb(goodsId, reduceCount);
                    DefaultRedisScript<Long> script = new DefaultRedisScript<>(ROLLBACK_LUA_SCRIPT, Long.class);
                    Long result = stringRedisTemplate.execute(script, Arrays.asList(getFrozenKey(goodsId), getAvailableKey(goodsId)), reduceCount.toString());
                    if (!Objects.equals(result, 1L)) {
                        throw new RuntimeException();
                    }
                    setStatusRollbacked(context);
                }catch (Throwable e){
                    transactionStatus.setRollbackOnly();
                }
            });
            return true;
        } finally {
            // 确保锁释放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    // ------------------- 数据库同步方法(带失败回滚) -------------------

    private void syncPrepareToDb(Long goodsId, Integer reduceCount) {
        int i = goodsStockMapper.decreaseAvailable(goodsId, reduceCount);
        if (i == 0) {
            throw new RuntimeException("数据库扣减可用库存失败, goodsId:" + goodsId);
        }
    }

    private void syncCommitToDb(Long goodsId, Integer reduceCount) {
        int i = goodsStockMapper.decreaseFrozen(goodsId, reduceCount);
        if (i == 0) {
            throw new RuntimeException("数据库扣减冻结库存失败, goodsId:" + goodsId);
        }
    }

    private void syncRollbackToDb(Long goodsId, Integer reduceCount) {
        int i = goodsStockMapper.increaseAvailable(goodsId, reduceCount);
        if (i == 0) {
            throw new RuntimeException("数据库恢复可用库存失败, goodsId:" + goodsId);
        }
    }


    // ------------------- 工具方法 -------------------

    /**
     * 获取商品可用库存的Redisson键
     */
    private String getAvailableKey(Long goodsId) {
        return STOCK_AVAILABLE_KEY + goodsId;
    }

    /**
     * 获取商品冻结库存的Redisson键
     */
    private String getFrozenKey(Long goodsId) {
        return STOCK_FROZEN_KEY + goodsId;
    }

    /**
     * 获取商品分布式锁的键
     */
    private String getLockKey(Long goodsId) {
        return STOCK_LOCK_KEY + goodsId;
    }

    private RBucket<Integer> getFenceBucket(BusinessActionContext context) {
        String xid = context.getXid();
        long branchId = context.getBranchId();
        String actionName = context.getActionName();
        return redissonClient.getBucket("tcc:" + actionName + ":" + xid + ":" + branchId);
    }

    private Integer getStatus(BusinessActionContext context) {
        return getFenceBucket(context).get();
    }

    private void setStatusTried(BusinessActionContext context) {
        getFenceBucket(context).set(STATUS_TRIED);
    }

    private void setStatusCommitted(BusinessActionContext context) {
        getFenceBucket(context).set(STATUS_COMMITTED);
    }

    private void setStatusRollbacked(BusinessActionContext context) {
        getFenceBucket(context).set(STATUS_ROLLBACKED);
    }

    private void setStatusSuspended(BusinessActionContext context) {
        boolean b = getFenceBucket(context).setIfAbsent(STATUS_SUSPENDED);
    }
}

local availableKey = KEYS[1]  
local frozenKey = KEYS[2]     
local need = tonumber(ARGV[1])
local available = tonumber(redis.call('get', availableKey) or 0)
local frozen = tonumber(redis.call('get', frozenKey) or 0)
if available >= need then
    redis.call('decrby', availableKey, need)
    redis.call('incrby', frozenKey, need)
    return 1
end
return 0
local frozenKey = KEYS[1]
local frozen = tonumber(redis.call('get', frozenKey) or 0)
local need = tonumber(ARGV[1])
if frozen >= need then
    redis.call('decrby', frozenKey, need)
    return 1
end
return 0
local frozenKey = KEYS[1]
local availableKey = KEYS[2]
local frozen = tonumber(redis.call('get', frozenKey) or 0)
local need = tonumber(ARGV[1])
if frozen >= need then
    redis.call('decrby', frozenKey, need)
    redis.call('incrby', availableKey, need)
    return 1
end
return 0
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值