Redis秒杀案例


在商品限量秒杀抢购的场景,一定会遇到抢购成功数超过限量的问题和高并发的情况影响系统性能

1、虽然能用数据库的锁避免超过限量的问题。但是在高并发的情况下,大大影响数据库性能

2、为了避免并发操作数据库,我们可以使用队列来限制,但是并发量会让队列内存瞬间升高

3、可以用悲观锁来实现,但是这样会造成用户等待,响应慢体验不好

一. 相关名词

  • 悲观锁:即实际对某个商品的购买api,同时只允许一个用户访问,“查询该商品数量”、“商品数量减1”是在同一个事务中,保证数据的完整性。缺点是性能,通常无法满足抢购的场景,因为只允许一个用户访问,既当前用户操作完,下一个用户才能进来。
  • FIFO: 客户端的抢购指令,只是插入一个交易表,由另外一个统一的线程来处理交易表,标记交易的成功或失败(如商品已售完)。缺点是客户端无法立即得到反馈,需要等待统一的线程处理完自己的交易后才知道抢购是否成功。此种方法也是可行的。
  • 乐观锁:即每个抢购指令前:step 1. 首先做个特殊标记; step 2. 然后正常执行指令; step 3. 在指令提交时,根据标记判断step 1至step 3之间商品数据是否有变化,如果有,则失败;否则,则抢购成功。

二. 秒杀问题

秒杀场景中,客户端对服务器的访问可以抽象为两个:

  1. 访问静态页面(列出静态商品页面) 2. 访问后台接口(抢购)

静态页面可以使用DNS实现,压力不大;

而后台接口是重点要解决的问题: (1) 响应一定要快 (2) 不要直接访问传统数据库,太慢。建议使用内存数据库技术,如redis等 (3) 防止同一账号短时间内的多次请求 (4) 防止超发(即本来限量只有100件商品,却最终成交了101件)

三. 解决案例

案例一:单线程秒杀抢购

    <!--导入依赖-->
	<dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>

代码实现:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;
import java.util.UUID;

/**
 * @author : xsh
 * @describe: 单线程抢购
 */
public class seckillDemo1 {
    public static void main(String[] arg) throws Exception {
        Jedis jedis = null;
        try {
            jedis = new Jedis("127.0.0.1",6379);// 获取jedis连接
            String key_s = "user_name";// 抢到的用户
            String key = "count";// 商品数量
            String clientName = UUID.randomUUID().toString().replace("-", "");// 用户名字
            jedis.set(key,"20");//设置抢购商品数量
            while (true) {
                try {
                    jedis.watch(key);// 监听key,为key加上乐观锁
                    System.out.println("用户:" + clientName + "开始抢商品");
                    System.out.println("当前商品的个数:" + jedis.get(key));
                    int prdNum = Integer.parseInt(jedis.get(key));// 当前商品个数
                    if (prdNum > 0) {
                        Transaction transaction = jedis.multi();// 标记一个事务块的开始
                        transaction.set(key, String.valueOf(prdNum - 1));
                        List<Object> result = transaction.exec();// 原子性提交事物
                        if (result == null || result.isEmpty()) {
                            // watch-key被外部修改,或者是数据操作被驳回
                            System.out.println("用户:" + clientName + "没有抢到商品");
                        } else {
                            jedis.sadd(key_s, clientName);// 将抢到的用户存起来
                            System.out.println("用户:" + clientName + "抢到商品");
                        }
                    } else {
                        System.out.println("库存为0,用户:" + clientName + "没有抢到商品");
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {

                    jedis.unwatch();// exec,discard,unwatch命令都会清除连接中的所有监视

                }
            } // while
        } catch (Exception e) {
            // TODO: handle exception
            System.out.println("redis bug:" + e.getMessage());
        } finally {
            // 释放jedis连接
            try {
                jedis.close();
            } catch (Exception e) {
                System.out.println("redis bug:" + e.getMessage());
                // TODO Auto-generated catch block
            }
        }
    }
}

案例二:多线程秒杀抢购

实现原理:

(1) 初始化redisKey(已抢商品数量)为0,利用redis的watch功能,监控这个redisKey的状态值

(2) 获取redisKey的值,当redisKey大于限购数时,停止抢购

(3) 创建redis事务,每次并发给redisKey的值+1

(4) 然后去执行这个事务,如果key的值被修改过,说明数据已经被其它线程更改,此时key不+1

    <!--导入依赖-->
	<dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>

代码实现:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author : xsh
 * @describe:
 *      1、利用redis的watch功能,监控这个redisKey的状态值
 *      2、获取redisKey的值
 *      3、创建redis事务
 *      4、给这个key的值+1
 *      5、然后去执行这个事务,如果key的值被修改过,说明数据已经被其它线程更改,key不+1
 */
public class seckillDemo2 {
    public static void main(String[] arg){
        String redisKey = "redisKey";
        //开启20个线程
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        try {
            Jedis jedis = new Jedis("127.0.0.1",6379);
            jedis.set(redisKey,"0");
            jedis.close();
        }catch (Exception e){
            e.printStackTrace();
        }

        for (int i=0;i<500;i++){ //此处五百并发,可以更大
            executorService.execute(()->{
                Jedis jedis1 = new Jedis("127.0.0.1",6379);
                try {
                    jedis1.watch(redisKey);
                    String redisValue = jedis1.get(redisKey);
                    int valInteger = Integer.valueOf(redisValue);
                    String userInfo = UUID.randomUUID().toString();
                    if (valInteger<20){
                        Transaction transaction = jedis1.multi();  //multi开始事务
                        //incr(redisKey),对redisKey的value值+1
                        transaction.incr(redisKey);
                        List<Object> exec = transaction.exec();//exec执行事务

                        if (exec.size()!=0){
                            System.out.println("用户:"+userInfo+",秒杀成功!当前成功人数:"+(valInteger+1));
                        }else {
                            System.out.println("用户:"+userInfo+",秒杀失败");
                        }
                    }else {
                        System.out.println("已经有20人秒杀成功,秒杀结束");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    jedis1.close();
                }
            });
        }
        executorService.shutdown();
    }
}

运行结果:最终成功人数只有20个,抢购成功人数满20后,秒杀结束。

案例三:多线程抢购优化

商品限量100,20个线程池,1000并发,并将抢购结果写入Redis数据库

MyRunnable.java:实现秒杀逻辑

package seckill3;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;

/**
 * @author : xsh
 * @create : 2020-01-03 - 14:36
 */
public class MyRunnable implements Runnable {
 
    String watchkeys = "watchkeys";// 监视keys
    
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    String userinfo;
    public MyRunnable() {
    }
    public MyRunnable(String uinfo) {
        this.userinfo=uinfo;
    }
    @Override
    public void run() {
        try {
            jedis.watch(watchkeys);// watchkeys
 
            String val = jedis.get(watchkeys);
            int valint = Integer.valueOf(val);
            
            if (valint <= 100 && valint>=1) {
            
                 Transaction tx = jedis.multi();// 开启事务
               // tx.incr("watchkeys");
                tx.incrBy("watchkeys", -1);
 
                List<Object> list = tx.exec();// 提交事务,如果此时watchkeys被改动了,则返回null
                 
                if (list == null ||list.size()==0) {
 
                    String failuserifo = "fail"+userinfo;
                    String failinfo="用户:" + failuserifo + "商品争抢失败,抢购失败";
                    System.out.println(failinfo);
                    /* 抢购失败业务逻辑 */
                    jedis.setnx(failuserifo, failinfo);
                } else {
                    for(Object succ : list){
                         String succuserifo ="succ"+succ.toString() +userinfo ;
                         String succinfo="用户:" + succuserifo + "抢购成功,当前抢购成功人数:"
                                 + (1-(valint-100));
                         System.out.println(succinfo);
                         /* 抢购成功业务逻辑 */
                         jedis.setnx(succuserifo, succinfo);
                    }
                }

            } else {
                String failuserifo ="kcfail" +  userinfo;
                String failinfo1="用户:" + failuserifo + "商品被抢购完毕,抢购失败";
                System.out.println(failinfo1);
                jedis.setnx(failuserifo, failinfo1);
                //Thread.sleep(500);
                return;
            }
 
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        } 
    }          
}

MyRedistest.java:主方法

package seckill3;

import redis.clients.jedis.Jedis;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author : xsh
 * @create : 2020-01-03 - 14:32
 * @describe:
 */
public class MyRedistest {
   public static void main(String[] args) {
        final String watchkeys = "watchkeys";
        ExecutorService executor = Executors.newFixedThreadPool(20); //20个线程池并发数

        final Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.set(watchkeys, "100");//设置起始的抢购数
        // jedis.del("setsucc", "setfail");
        jedis.close();
        for (int i = 0; i < 1000; i++) {//设置1000个人来发起抢购
            executor.execute(new MyRunnable("user"+getRandomString(6)));
        }
        executor.shutdown();
}

/*根据长度随机生成对应长度的字母串*/
public static String getRandomString(int length) { //length是随机字符串长度
       String base = "abcdefghijklmnopqrstuvwxyz0123456789";
       Random random = new Random();
       StringBuffer sb = new StringBuffer();
       for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }
}

运行结果:

且数据成功存入Redis:

案例四:综合案例(常用)

本例采用异步方式记录交易log表,之所以要插入此log表,是为了方便统计最终商品交易的成功数、失败数。所以需引入mysql驱动包。

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>

需创建test数据库,在test数据库内创建t_buy表存放购买结果,表内有两个字段:user(varchar),result(int)

MyJedisPool.java

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @author : xsh
 * @describe: Redis客户端pool的实现
 */
public class MyJedisPool {

    private static JedisPool pool;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        // 设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数)
        config.setEvictionPolicyClassName("org.apache.commons.pool2.impl.DefaultEvictionPolicy");
        // 最大连接数
        config.setMaxTotal(8);
        // 最大空闲连接数
        config.setMaxIdle(8);
        // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,
        // 默认-1
        config.setMaxWaitMillis(-1);
        // 是否启用后进先出,默认true
        config.setLifo(true);
        // 最小空闲连接数, 默认0
        config.setMinIdle(0);
        // 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3
        config.setNumTestsPerEvictionRun(3);
        // 对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数
        // 时直接逐出,不再根据MinEvictableIdleTimeMillis判断 (默认逐出策略)
        config.setSoftMinEvictableIdleTimeMillis(1800000);
        // 在获取连接的时候检查有效性, 默认false
        config.setTestOnBorrow(false);
        // 在空闲时检查有效性, 默认false
        config.setTestWhileIdle(false);
        // 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
        config.setTimeBetweenEvictionRunsMillis(-1);

        pool = new JedisPool(config, "localhost");
    }

    public static Jedis getJedis() {
        return pool.getResource();
    }

    /** 归还jedis对象 */
    public static void recycleJedisOjbect(Jedis jedis) {
        jedis.close();
    }
}

Trade.java

/**
 * @author : xsh
 * @describe: 交易记录数据模型
 */
public class Trade {
    private String user;
    private int result;
    public int getResult() {
        return result;
    }
    public void setResult(int result) {
        this.result = result;
    }
    public String getUser() {
        return user;
    }
    public void setUser(String user) {
        this.user = user;
    }
}

LogManager.java

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * @author : xsh
 * @describe: 异步记录交易Log的服务
 */
public class LogManager implements Runnable {
    private static LinkedBlockingQueue<Trade> list = new LinkedBlockingQueue<Trade>();
    private static String url="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8";
    private static Connection conn;

    static {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection(url, "root","123456");
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }

        new Thread(new LogManager()).start();
    }

    public static void addLog(Trade log) {
        list.add(log);
    }

    @Override
    public void run() {
        while(true) {
            Trade trade = null;
            try {
                trade = list.take();
                log(trade);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void log(Trade trade) {
        String sql = "insert into t_buy (user, result) values(?, ?)";
        try {
            PreparedStatement pst = conn.prepareStatement(sql);
            pst.setString(1, trade.getUser());
            pst.setInt(2, trade.getResult());
            pst.execute();
        } catch(SQLException e) {
            e.printStackTrace();
        }
    }
}

FlashSaleTest.java

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;

/**
 * @author : xsh
 * @describe:  抢购模拟
 */
public class FlashSaleTest {
    private static String KEY = "COUNT";
    private int userCount;
    private int interval;

    /**
     * @param totalItemCount 商品总数
     * @param userCount 模拟用户数
     * @param interval 用户采购间隔(毫秒)
     */
    public FlashSaleTest(int totalItemCount, int userCount, int interval) {
        this.userCount = userCount;
        this.interval = interval;
        Jedis jedis = MyJedisPool.getJedis();
        jedis.set(KEY, "" + totalItemCount);
        MyJedisPool.recycleJedisOjbect(jedis);
    }

    public void start() {
        for(int i=0; i<userCount; i++) {
            Thread tt = new UserThread("Thread" + i);
            tt.start();
            try {
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static int buy() {
        Jedis jedis = MyJedisPool.getJedis();
        jedis.watch(KEY);
        int value = Integer.valueOf(jedis.get(KEY)).intValue();
        int result;
        if(value > 0) {
            Transaction tx = jedis.multi();
            tx.decr(KEY);
            List<Object> res = tx.exec();
            if(res.size() == 0) {
                result = 1; // 失败
            } else {
                result = 0; // 成功
            }
        } else {
            result = 2;  // 已售完
        }
        MyJedisPool.recycleJedisOjbect(jedis);
        return result;
    }

    static class UserThread extends Thread {
        private String user = null;
        public UserThread(String user) {
            this.user = user;
        }
        @Override
        public void run() {
            int result = buy();
            Trade trade = new Trade();
            trade.setUser(this.user);
            trade.setResult(result);
            LogManager.addLog(trade);
            System.out.println("user(" + user + ") result(" + result + ")");
        }
    }

    public static void main(String[] args) {
        FlashSaleTest test = new FlashSaleTest(100, 200, 100);
        test.start();
    }
}

运行FlashSaleTest.java中的main方法,并查看数据库:

select result, count(*) from t_buy group by result;
resultcount(*)
0100
14
296

可以看到:

  • 最终成功100件,和商品总数一致。所有商品被抢购完了,且没有发生“超发”
  • 抢购中失败4次,即抢购提交中,数据已经被其它线程更改,因此失败
  • 其它96次失败,是商品已经售罄
redis
  • 作者:管理员 (联系作者)
  • 发表时间:2020-01-04 02:04
  • 版权声明:自由转载-非商用-非衍生-保持署名(null)
  • undefined
  • 评论



    代做工资流水公司淄博自存银行流水阜阳制作房贷银行流水漳州银行流水账单模板东莞制作房贷流水泰安个人流水价格信阳贷款银行流水公司阜阳工资流水单多少钱西安做工资证明舟山查询工资银行流水孝感查贷款流水邯郸打印工资代付流水上海购房银行流水样本衡阳薪资银行流水查询漳州打印个人工资流水嘉兴背调银行流水查询泰州查薪资银行流水苏州打印企业流水打印信阳打房贷银行流水德阳对公银行流水公司株洲打银行流水账株洲背调流水价格洛阳代开企业对私流水上饶办理贷款工资流水惠州流水账单打印长沙贷款银行流水打印九江代开流水账单金华薪资流水单汕头代办企业对公流水潍坊开贷款工资流水岳阳房贷流水样本香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

    代做工资流水公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化