redisTemplate代替jedis实现分布式锁

本文介绍如何在SpringBoot项目中使用RedisTemplate实现分布式锁,解决并发场景下的数据一致性问题。文章详细解释了锁工具类的设计,包括锁的获取与释放逻辑,以及在实际业务流程中的应用案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言:

springBoot集成redis之后,再使用之前的jedis,会加上诸多配置和注解,很烦恼,因此,自己研究了两天,使用redisTemplate实现分布式锁。

1.锁工具类

/**
 * 分布式事务锁工具类
 *
 * @author leimin
 * @description
 * @time: 2019/09/20
 **/
@Component
@Slf4j
public class DistributeLockUtil {

    /**
     * 引入redisTemplate
     */
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 超时时间,8s
     */
    private static final long HANDLE_MSECS = 8000L;
    /**
     * 重复获取锁的时间间隔 50毫秒
     */
    private static final long TRY_INTERVAL = 50L;
    /**
     * 重复获取锁的超时时间 10s
     */
    private static final long TIME_OUT_MSECS = 10000L;

    /**
     * 获取锁
     *
     * @param lockName 锁的名称
     * @return 释放锁的时间的时间戳
     * @throws InterruptedException 中断异常
     */
    public Long getLock(String lockName) {

        long waitTime = System.currentTimeMillis() + TIME_OUT_MSECS;
        long lockTimeout;
        Object obj = redisTemplate.opsForValue().get(lockName);
        long expire = obj == null ? 0L : (Long) obj;

        // 时间段内,循环获取锁
        while (System.currentTimeMillis() < waitTime) {
            lockTimeout = System.currentTimeMillis() + HANDLE_MSECS + 1L;

            // 获取到锁后,设置过期时间
            if (redisTemplate.opsForValue().setIfAbsent(lockName, lockTimeout)) {
                redisTemplate.expire(lockName, HANDLE_MSECS, TimeUnit.MILLISECONDS);
                return lockTimeout;

                // 发现死锁,重置锁的过期时间
            } else if (expire < System.currentTimeMillis()) {
                redisTemplate.opsForValue().set(lockName, lockTimeout, HANDLE_MSECS, TimeUnit.MILLISECONDS);

                // 重置后,操作时间小于获取时间,允许等待操作时间
                // 操作时间大于获取时间,肯定获取不到,直接结束;
                //if (HANDLE_MSECS > TIME_OUT_MSECS) {
                //    return 0L;
                //}
                 return 0L;
            }

            try {
                Thread.sleep(TRY_INTERVAL);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return 0L;
    }

    /**
     * 释放锁的情况:
     *
     * 1.业务执行过程中,发生异常必须释放锁;(调用释放方法)
     * 2.业务执行完成,必须释放锁;(调用释放方法)
     * 3.发生死锁,必须释放锁;(超时设置)
     * 4.异步处理导致主程序过早释放锁;(取消finally,所有代码跑完才释放锁)
     */

    /**
     * 同步释放锁
     *
     * @param lockName    锁的名称
     * @param lockTimeout 超时时间
     */
    public void releaseLock(String lockName, long lockTimeout) {

        // 获取锁的释放时间, 如果锁的释放时间和定义的释放时间一致,那么释放锁
        Object lockTime = redisTemplate.opsForValue().get(lockName);
        long currentLockTime = lockTime == null ? 0L : (long) lockTime;

        if (lockTimeout != 0L && currentLockTime == lockTimeout) {
            redisTemplate.opsForValue().getOperations().delete(lockName);
        }
    }

    /**
     * 异步释放锁
     *
     * @param lockName 锁的名称
     */
    public void releaseLock(String lockName) {

        redisTemplate.opsForValue().getOperations().delete(lockName);
    }
}

2.使用锁

我的业务流程是异步的,因此,释放锁,必须在异步操作的最后

2.1 主业务
    /**
     * 表单数据,新增审批
     *
     * @param formId     表单id
     * @param grantId    操作员id
     * @param formDataId 表单中数据id
     * @param tenantId   企业id
     * @return 分装结果对象
     * @author leimin
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public BaseVO formCreateApproval(Long formId, Long grantId, Long formDataId, Long tenantId) {

        long lockTimeOut = 0L;
        try {
            lockTimeOut = distributeLockUtil.getLock(FormConstants.CREATE_APPROVAL_LOCK + tenantId + formDataId);
        } catch (Exception e) {
            return new BaseVO(BaseVO.CONFLICT_CODE, "对不起,目前系统繁忙,请稍后重试");
        }
        if (lockTimeOut == 0) {
            return new BaseVO(BaseVO.CONFLICT_CODE, "对不起,目前系统繁忙,请稍后重试");
        }

        try {
            FormData formData = formDao.getByFormDataId(formDataId, tenantId);
            if (!auth.get(FormDataState.APPROVAL_CREATE.toString(), grantId.equals(formData.getCreator()), formData.getStatus())
                    || !formData.getEditor().equals(grantId)) {
                return new BaseVO(BaseVO.UNAUTHORIZED_CODE, "无权限", null);
            }

            //通过权限审批,修改fromData中的数据,approval_create;插入formDataApproval对象,approving;
            setFormDataStatusAndIndex(formData, FormDataState.APPROVAL_CREATE);
            FormDataApproval formDataApproval = createFormDataApproval(formData, FormDataApprovalState.APPROVING);
            formDao.updateFormDataStatus(formData);
            formApprovalDao.upsertFormDataApproval(formDataApproval);

            //日志记录准备数据 jsonObject.toJSONString()
            loggerUtil.saveLog(tenantId, grantId, formData, FormConstants.CREATE);

            //调用工作流中提交审批接口
            return audit.audit(formData, formId, grantId, formDataId, tenantId, FormConstants.CREATE, formDataApproval.getId());
        } catch (Exception e) {
            distributeLockUtil.releaseLock(FormConstants.CREATE_APPROVAL_LOCK + tenantId + formDataId, lockTimeOut);
            return new BaseVO(BaseVO.OTHER_CODE, "操作失败!");
        }
    }
2.2异步业务
    /**
     * 审批完成后,回调接口,修改源数据
     *
     * @param tenantId 企业id
     * @param formId   表单id
     * @param dataId   数据id
     * @param result   审核结果
     * @param type     审核类型
     * @return 返回的结果
     * @author niulibing
     */
    @Override
    public BaseVO callback(Long tenantId, Long formId, Long dataId, String result, String type) {

        try {
            //根据dataId查询formDataApproval表的数据,查询状态为审核中的状态的数据
            FormDataApproval formDataApproval = formApprovalDao.findOneByDataId(tenantId, dataId);

            if (formDataApproval == null) {
                return new BaseVO(BaseVO.OTHER_CODE, "不存在该条记录");
            }

            //审核类型:create
            switch (type) {

                //审核类型:create
                case FormConstants.CREATE:
                    return createApprovalCallBack(tenantId, formId, dataId, result, formDataApproval);

                //审核类型:edit
                case FormConstants.EDIT:
                    return editApprovalCallBack(tenantId, formId, dataId, result, formDataApproval);

                //审核类型:delete
                case FormConstants.DELETE:
                    return deleteApprovalCallBack(tenantId, formId, dataId, result, formDataApproval);
                default:
                    return new BaseVO(BaseVO.OTHER_CODE, "审核方式不存在,仅支持create、edit、delete,请确认后提交。");
            }
        } finally {
            switch (type) {
                case FormConstants.CREATE:
                    distributeLockUtil.releaseLock(FormConstants.CREATE_APPROVAL_LOCK + tenantId + dataId);
                    break;
                case FormConstants.EDIT:
                    distributeLockUtil.releaseLock(FormConstants.EDIT_APPROVAL_LOCK + tenantId + dataId);
                    break;
                default:

            }
        }

    }

redisTemplate相关方法参考:https://www.cnblogs.com/yanan7890/p/6617305.html

<think>为了编写一个高并发的抢票程序,我们需要考虑多个方面:并发控制、数据库操作、避免超卖、系统性能等。下面是一个基于Java的简单实现,使用Spring Boot框架和MySQL数据库,并利用Redis进行分布式锁管理来避免超卖问题。 步骤: 1. 数据库设计:创建票(ticket)和订单(order)表。 2. 使用Redis分布式锁(或数据库乐观锁)来保证并发下的数据一致性。 3. 使用线程池或消息队列来缓解高并发压力(这里为了简化,我们先使用分布式锁)。 注意:在实际高并发场景下,可能需要使用缓存(如Redis)来存储票的库存,利用其原子操作(如decrement)来减少数据库压力,并配合消息队列进行异步下单。 这里我们采用Redis分布式锁来控制库存的扣减,同时使用数据库事务保证数据一致性。 首先,我们设计数据库表: 票表(ticket): id: 主键 name: 票名称 stock: 库存 订单表(ticket_order): id: 主键 ticket_id: 票ID user_id: 用户ID create_time: 下单时间 然后,我们实现一个抢票接口,步骤: 1. 用户请求抢票,传入票ID和用户ID。 2. 尝试获取Redis分布式锁(锁的key可以是票ID,避免不同票之间的锁竞争)。 3. 获取锁后,查询票的库存。 4. 如果库存大于0,则减少库存,创建订单。 5. 释放锁。 但是,直接使用分布式锁可能会成为性能瓶颈,因为同一时间只能有一个请求操作某个票的库存。我们可以考虑使用分段锁或者将库存放到Redis中利用其原子操作。 另一种更高效的方式是使用Redis的原子操作(如DECR)来扣减库存,这样就不需要锁,因为Redis是单线程执行命令的。但是,这样需要保证库存数据在Redis和数据库之间的同步。 这里我们采用两种方式结合: 1. 将库存预热到Redis中。 2. 使用Redis的DECR操作来扣减库存,如果扣减后库存大于等于0,则允许下单。 3. 然后通过消息队列异步将库存同步到数据库,并创建订单(为了简化,我们同步操作数据库)。 但是,考虑到题目要求用Java写,且需要高并发,我们这里先使用Redis分布式锁的方式(因为更直接,且容易理解),后续可以优化。 由于篇幅限制,我们这里使用Redis分布式锁的方式,并同步更新数据库。 依赖: Spring Boot Web Spring Data JPA Spring Data Redis 实现: 1. 配置Redis连接。 2. 创建Ticket和TicketOrder实体。 3. 创建Repository。 4. 编写抢票服务,使用Redis分布式锁。 代码示例: 注意:这里使用RedisTemplate操作Redis,使用Redisson或Lettuce的分布式锁更佳,但为了简单,我们使用SETNX命令实现一个简单的分布式锁(并设置过期时间防止死锁)。 由于时间关系,我们使用Redisson客户端(需要添加依赖),因为它提供了更完善的分布式锁实现。 添加Redisson依赖: <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.16.1</version> </dependency> 服务层代码:</think>### 高并发抢票程序 Java 实现 下面是一个基于 Java 的高并发抢票程序实现,使用 Redis + MySQL 架构,结合分布式锁和缓存优化: ```java // 主要依赖 import redis.clients.jedis.Jedis; import redis.clients.jedis.params.SetParams; import java.sql.*; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TicketSystem { // Redis 配置 private static final String REDIS_HOST = "localhost"; private static final int REDIS_PORT = 6379; private static final String TICKET_KEY = "ticket:stock"; // MySQL 配置 private static final String DB_URL = "jdbc:mysql://localhost:3306/ticket_db"; private static final String DB_USER = "root"; private static final String DB_PASSWORD = "password"; // 分布式锁配置 private static final String LOCK_KEY = "ticket_lock"; private static final int LOCK_EXPIRE = 3000; // 3秒 public static void main(String[] args) { // 初始化票库存 initStock(1000); // 创建线程池模拟并发请求 ExecutorService executor = Executors.newFixedThreadPool(100); for (int i = 0; i < 5000; i++) { executor.execute(() -> { String userId = UUID.randomUUID().toString(); grabTicket(userId); }); } executor.shutdown(); } // 抢票核心逻辑 public static void grabTicket(String userId) { Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT); String requestId = UUID.randomUUID().toString(); try { // 获取分布式锁 if (acquireLock(jedis, LOCK_KEY, requestId, LOCK_EXPIRE)) { // 检查库存 long stock = Long.parseLong(jedis.get(TICKET_KEY)); if (stock <= 0) { System.out.println(userId + " 抢票失败,库存不足"); return; } // 扣减库存 jedis.decr(TICKET_KEY); // 释放锁 releaseLock(jedis, LOCK_KEY, requestId); // 创建订单 createOrder(userId); System.out.println(userId + " 抢票成功"); } else { System.out.println(userId + " 获取锁失败"); } } finally { jedis.close(); } } // 获取分布式锁 private static boolean acquireLock(Jedis jedis, String lockKey, String requestId, int expireTime) { SetParams params = SetParams.setParams().nx().px(expireTime); return "OK".equals(jedis.set(lockKey, requestId, params)); } // 释放分布式锁 private static void releaseLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) " + "else return 0 end"; jedis.eval(script, 1, lockKey, requestId); } // 创建订单 private static void createOrder(String userId) { try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); PreparedStatement stmt = conn.prepareStatement( "INSERT INTO orders(user_id, ticket_id) VALUES(?, ?)")) { stmt.setString(1, userId); stmt.setInt(2, 1); // 假设只有一种票 stmt.executeUpdate(); } catch (SQLException e) { System.err.println("创建订单失败: " + e.getMessage()); } } // 初始化库存 private static void initStock(int stock) { try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) { jedis.set(TICKET_KEY, String.valueOf(stock)); } try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); Statement stmt = conn.createStatement()) { stmt.execute("CREATE TABLE IF NOT EXISTS orders (" + "id INT AUTO_INCREMENT PRIMARY KEY," + "user_id VARCHAR(255) NOT NULL," + "ticket_id INT NOT NULL)"); } catch (SQLException e) { e.printStackTrace(); } } } ``` ### 代码解释 1. **架构设计**: - **Redis**:存储实时库存(内存操作,高性能) - **MySQL**:持久化订单数据(保证数据可靠性) - **分布式锁**:使用 Redis 的 SETNX 命令实现,避免超卖 2. **核心组件**: - `acquireLock()`:获取分布式锁(使用 NX 和 PX 参数确保原子性) - `releaseLock()`:使用 Lua 脚本释放锁(保证原子操作) - `grabTicket()`:抢票主逻辑(检查库存→扣减→下单) - `createOrder()`:数据库订单创建 3. **并发控制**: - 线程池模拟高并发请求 - Redis 原子操作(decr)保证库存准确性 - 锁超时机制防止死锁 4. **防超卖策略**: - 先检查后扣减的库存操作 - 分布式锁保证单线程操作库存 - Redis 的原子递减操作 ### 优化建议 1. **性能优化**: ```java // 使用 Redis Pipeline 批量操作 Pipeline pipeline = jedis.pipelined(); pipeline.decr(TICKET_KEY); pipeline.sync(); // 使用连接池代替每次新建连接 JedisPool pool = new JedisPool(REDIS_HOST, REDIS_PORT); try (Jedis jedis = pool.getResource()) { ... } ``` 2. **库存预热**: ```java // 初始化时将库存加载到 Redis public static void preheatStock(int ticketId, int stock) { String key = "ticket:" + ticketId; jedis.set(key, String.valueOf(stock)); } ``` 3. **限流措施**: ```java // 使用 Redis 实现简单限流 public boolean allowRequest(String userId) { String key = "rate_limit:" + userId; long now = System.currentTimeMillis(); long count = jedis.zcount(key, now - 1000, now); if (count < 10) { // 每秒10次 jedis.zadd(key, now, UUID.randomUUID().toString()); return true; } return false; } ``` 4. **异步处理**: ```java // 使用消息队列解耦 public void asyncCreateOrder(String userId) { kafkaProducer.send(new ProducerRecord<>("order_topic", userId)); } ``` ### 关键注意事项 1. **分布式锁陷阱**: - 必须设置过期时间防止死锁 - 使用唯一标识(requestId)避免误删锁 - Lua 脚本保证原子性 2. **缓存与数据库一致性**: - 使用最终一致性策略 - 通过消息队列补偿数据库操作 - 定期同步库存 3. **故障处理**: - Redis 宕机时降级到数据库库存检查 - 重试机制处理网络抖动 - 库存回滚机制 4. **压力测试指标**: - 单节点 QPS 应达到 5000+ - 库存误差率 < 0.001% - 平均响应时间 < 50ms
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值