Spring Boot基于redis分布式锁模拟直播秒杀场景

摘要:Spring Boot基于redis分布式锁模拟秒杀场景,未完待续

§前言

  在Java中,关于锁我想大家都很熟悉,例如synchronized和Lock等。在并发编程中,我们通过加锁来保证数据一致。但是Java中的锁,只能保证在同一个JVM进程内中执行,如果在分布式集群环境下,就缴械投降,那如何处理呢?使用Redis锁来处理。

  测试用例所用软件开发环境如下:

  ♦ java version 13.0.1
  ♦ IntelliJ IDEA 2019.3.2 (Ultimate Edition)
  ♦ Spring Boot 2.3.0.RELEASE
  ♦Redis 5.0.10(暂时使用单台部署,后面使用集群)

§案例分析

  模拟一个比较常见的秒杀场景,假如只有1000件商品上架,这时候就需要用到锁。在《Spring Boot 整合Jedis连接Redis和简单使用》的JedisUtil基础上增加加锁和解锁函数:

package com.eg.wiener.utils;

import com.eg.wiener.config.JedisPoolFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.Map;

@Component
public class JedisUtil {
    private static Logger logger = LoggerFactory.getLogger(JedisUtil.class);

    private static String lock_key = "lock_"; //锁键
    private static String lock_ok = "OK"; //锁键
    protected static long internalLockLeaseTime = 30000;//锁过期时间
    private long timeout = 999999; //获取锁的超时时间
    private static String lua_script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    //SET命令的参数
    private final static SetParams params = SetParams.setParams().nx().px(internalLockLeaseTime);

    @Autowired
    private JedisPool jedisPool;
    @Autowired
    private JedisPoolFactory jedisPoolFactory;

    /**
     * 存储字符串键值对,永久有效
     * @param key
     * @param value
     * @return
     * @author hw
     * @date 2018年12月14日
     */
    public String set(String key, String value) {
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.set(key, value);
        } catch (Exception e) {
            return "-1";
        } finally {
            jedis.close();
        }
    }

    /**
     * 根据传入key获取指定Value
     * @param key
     * @return
     * @author hw
     * @date 2018年12月14日
     */
    public String get(String key) {
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.get(key);
        } catch (Exception e) {
            return "-1";
        } finally {
            jedis.close();
        }
    }

    /**
     * 删除字符串键值对
     * @param key
     * @return
     * @author hw
     * @date 2018年12月14日
     */
    public Long del(String key) {
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.del(key);
        } catch (Exception e) {
            return -1L;
        } finally {
            jedis.close();
        }
    }
    /**
     * 校验Key值是否存在
     */
    public Boolean exists(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.exists(key);
        } catch (Exception e) {
            return false;
        } finally {
            jedis.close();
        }
    }
    /**
     * 分布式锁
     * @param key
     * @param value
     * @param time 锁的超时时间,单位:秒
     *
     * @return 获取锁成功返回"OK",失败返回null
     */
    public String getDistributedLock(String key,String value,int time){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.set(key, value, new SetParams().nx().ex(time));
        } catch (Exception e) {
            return null;
        } finally {
            jedis.close();
        }
    }

    /**
     * 加锁
     *
     * @param business_code 业务编码
     * @param id
     * @return
     */
    public boolean lock(String business_code, String id) {
        Jedis jedis = jedisPool.getResource();
        Long start = System.currentTimeMillis();
        try {
            while (true) {
                //SET命令返回OK ,则证明获取锁成功
                String lock = jedis.set(lock_key.concat(business_code), id, params);
                if (lock_ok.equals(lock)) {
                    return true;
                }
                //否则循环等待,在timeout时间内仍未获取到锁,则获取失败
                long waitTime = System.currentTimeMillis() - start;
                if (waitTime >= timeout) {
                    return false;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    logger.error("sleep失败,", e);
                }
            }
        } finally {
            jedis.close();
        }
    }

    /**
     * 解锁
     *
     * @param id
     * @return
     */
    public boolean unlock(String business_code, String id) {
        Jedis jedis = jedisPool.getResource();
        try {
            Object result = jedis.eval(lua_script,
                    Collections.singletonList(lock_key.concat(business_code)),
                    Collections.singletonList(id));
            if ("1".equals(result.toString())) {
                return true;
            }
            return false;
        } finally {
            jedis.close();
        }
    }
    public Map<String, String> getMap(String key) {
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.hgetAll(key);
        } catch (Exception e) {
            return null;
        } finally {
            jedis.close();
        }
    }
    public Long setMap(String key, Map<String, String> value) {
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.hset(key, value);
        } catch (Exception e) {
            logger.info("-------向Redis存入Map失败--------", e);
            return -1L;
        } finally {
            jedis.close();
        }
    }
}

解锁是通过jedis.eval来执行一段LUA来实现的,这是当下流行的靠谱方案,它把锁的Key键和生成的字符串当做参数传入。

在UserController中添加测试函数,模拟直播秒杀场景,上架商品数量右主播设置。由于是模拟是否加锁解锁成功,为了简化,故在程序中自动生成客户id。

    
    private int count = 0;

    /**
     * 模拟直播秒杀场景,上架商品数量限指定件
     *
     * @param productNum 上架商品数量
     * @return
     * @throws InterruptedException
     */
    @ApiOperation(value = "测试Redis分布式锁")
    @GetMapping("/testRedisLock")
    @ResponseBody
    public String testRedisLock(Integer productNum) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(productNum);

        ExecutorService executorService = Executors.newFixedThreadPool(100);
        long start = System.currentTimeMillis();
        for (int i = 0; i < productNum; i++) {
            executorService.execute(() -> {
                String businessCode = "test";
                // 模拟下单用户,其实作为入参,为了简化,故自动生成
                String id = UUID.randomUUID().toString();
                try {
//                    jedisUtil.lock(businessCode, id);
                    jedisUtil.getDistributedLock(businessCode, id, 1);
                    count++;
                } finally {
                    jedisUtil.unlock(businessCode, id);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        String ret = String.format("执行线程数:%s,总耗时:%s,count数为:%s", productNum, System.currentTimeMillis() - start, count);
        logger.info(ret);
        return ret;
    }    

由代码可知,商品售罄即止Swagger3测试效果如图所示:

大家可以试试另外一种加锁方式,你有Redis分布式锁的更佳实现方案吗?欢迎留言!

§小结

  对于上述应用单节点Redis分布式锁模拟直播秒杀场景,您怎么看?欢迎参与话题互动讨论,分享你的观点和看法, 评论区留言哦,喜欢小编文章的朋友请点赞,谢谢你的参与!

posted @ 2021-02-28 16:54  楼兰胡杨  阅读(333)  评论(0编辑  收藏  举报