记录自己的redis之路-04-redis秒杀实例

乐观锁的实现

乐观锁实现中的锁就是商品的键值对。使用jediswatch方法监视商品键值对,如果事务提交exec时发现监视的键值对发生变化,事务将被取消,商品数目不会被改动。

    1. 创建MyRunnable 实现Runnable 接口
package com.qrcode.redisdemo.redis02;

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

import java.util.List;

public class MyRunnable implements Runnable {
    String watchkeys = "iphoneXS";// 监视keys
    Jedis jedis = new Jedis("192.168.253.128", 6379);
    String userinfo;

    public MyRunnable() {
    }

    public MyRunnable(String uinfo) {
        this.userinfo = uinfo;
    }

    @Override
    public void run() {
        try {

            jedis.watch(watchkeys); //当事务执行之前这个key发生了改变,事务会被打断

            String val = jedis.get(watchkeys); // 获取缓存中的数量
            int valint = Integer.valueOf(val);

            if (valint <= 100 && valint >= 1) {

                Transaction tx = jedis.multi();// 开启事务

                tx.incrBy(watchkeys, -1);  //减少一个商品数量

                List<Object> list = tx.exec();// 提交事务,如果此时watchkeys被改动了,则返回null

                if (list == null || list.size() == 0) {

                    String failinfo = "用户:" + userinfo + "商品争抢失败,抢购失败";
                      System.out.println(failinfo);
                    /* 抢购失败业务逻辑 */
//                    jedis.setnx(userinfo, failinfo);
                } else {
                    for (Object succ : list) {
                        String succinfo = "用户:" + userinfo + "抢购争抢成功,当前抢购成功人数:"
                                + (1 - (valint - 100));
                        System.out.println(succinfo);
                         /* 抢购成功业务逻辑 */
                         jedis.lpush("list1",userinfo);
//                        jedis.setnx(userinfo, succinfo);
                    }

                }

            } else {
                String failinfo1 = "用户:" + userinfo + "商品被抢购完啦!!!";
                System.out.println(failinfo1);
                return;
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }

    }
}
  1. 创建线程启动测试
package com.qrcode.redisdemo.redis02;

import com.qrcode.redisdemo.util.GuoUtil;
import redis.clients.jedis.Jedis;

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

public class MyRedistest {
    public static void main(String[] args) {
        //创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
        //线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,
        // 那么线程池会补充一个新线程。
        ExecutorService executor = Executors.newFixedThreadPool(20);  //20个线程池并发数

        final Jedis jedis = new Jedis("192.168.253.128", 6379);
        jedis.set("iphoneXS", "100");//设置起始的抢购数
        jedis.del("list1");
        jedis.close();

        for (int i = 0; i < 1000; i++) {//设置1000个人来发起抢购
            executor.execute(new MyRunnable("user"+i));
        }
        executor.shutdown();
    }
}

 

运行结果

记录自己的redis之路-04-redis秒杀实例

记录自己的redis之路-04-redis秒杀实例

记录自己的redis之路-04-redis秒杀实例

问题:

100人抢购,结果:

记录自己的redis之路-04-redis秒杀实例

记录自己的redis之路-04-redis秒杀实例

一百个 抢购 一百个 手机 ,竟然还有83个没有卖出去…

解决办法:

在抢购失败的时候让线程等待500毫秒,继续递归调用方法

记录自己的redis之路-04-redis秒杀实例

此时没问题了..

但是内心总有一股不安…TODO 后期继续搞.

 

记录自己的redis之路-04-redis秒杀实例

 

 

 

技术总结:

  • 线程池的使用

ExecutorService executor = Executors.newFixedThreadPool(20);

一般是和事务一起使用,当对某个key进行watch后如果其他的客户端对这个key进行了更改,那么本次事务会被取消,事务的exec会返回null

jedis.watch(watchkeys);

Transaction tx = jedis.multi();// 开启事务

tx.incrBy(watchkeys, -1)//减少一个商品数量

List<Object> list = tx.exec();// 提交事务,如果此时watchkeys被改动了,则返回null

悲观锁的实现

悲观锁中的锁是一个唯一标识的锁lockKey和该锁的过期时间。首先确定缓存中有商品,然后在拿数据(商品数目改动)之前先获取到锁,之后对商品数目进行减一操作,操作完成释放锁,一个秒杀操作完成。这个锁是基于redissetNX操作实现的阻塞式分布式锁

  1. 修改MyRunnable
package com.qrcode.redisdemo.redis02;

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

import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable {
    String watchkeys = "iphoneXS";// 监视keys
    Jedis jedis = new Jedis("192.168.253.128", 6379);
    String userinfo;
    Lock lock;
    public MyRunnable() {
    }

    public MyRunnable(String uinfo,Lock lock) {
        this.userinfo = uinfo;
        this.lock = lock;
    }

    @Override
    public void run() {
        lock.lock(); // 获得锁
        try {
            String val = jedis.get(watchkeys); // 获取缓存中的数量
            int valint = Integer.valueOf(val);
            if (valint <= 100 && valint >= 1) {
                jedis.set(watchkeys,String.valueOf((valint-1)));
                System.out.println("用户"+userinfo +"抢购成功,还剩"+(valint-1));
            }else {
                System.out.println("用户"+userinfo +"抢购失败");
            }
        }finally {
            lock.unlock(); // 释放锁
        }
}
  1. 修改线程启动测试
package com.qrcode.redisdemo.redis02;

import com.qrcode.redisdemo.util.GuoUtil;
import redis.clients.jedis.Jedis;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRedistest {
    public static void main(String[] args) {
        //创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
        //线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,
        // 那么线程池会补充一个新线程。
        ExecutorService executor = Executors.newFixedThreadPool(20);  //20个线程池并发数

        final Jedis jedis = new Jedis("192.168.253.128", 6379);
        jedis.set("iphoneXS", "100");//设置起始的抢购数
        jedis.del("list1");
        jedis.close();

//重入锁
        Lock lock=new ReentrantLock();
        for (int i = 0; i < 1000; i++) {//设置1000个人来发起抢购
            executor.execute(new MyRunnable("user"+i,lock));
        }
        executor.shutdown();
    }
}

结果:

记录自己的redis之路-04-redis秒杀实例

100人抢购  完美!

一步一个脚印 , 独自前行的路上不要急于求成.