记录自己的redis之路-04-redis秒杀实例
乐观锁的实现
乐观锁实现中的锁就是商品的键值对。使用jedis的watch方法监视商品键值对,如果事务提交exec时发现监视的键值对发生变化,事务将被取消,商品数目不会被改动。
-
- 创建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();
}
}
}
- 创建线程启动测试
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();
}
}
运行结果
问题:
100人抢购,结果:
一百个 抢购 一百个 手机 ,竟然还有83个没有卖出去…
解决办法:
在抢购失败的时候让线程等待500毫秒,继续递归调用方法
此时没问题了..
但是内心总有一股不安…TODO 后期继续搞.
技术总结:
- 线程池的使用
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和该锁的过期时间。首先确定缓存中有商品,然后在拿数据(商品数目改动)之前先获取到锁,之后对商品数目进行减一操作,操作完成释放锁,一个秒杀操作完成。这个锁是基于redis的setNX操作实现的阻塞式分布式锁。
- 修改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(); // 释放锁
}
}
- 修改线程启动测试
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();
}
}
结果:
100人抢购 完美!
一步一个脚印 , 独自前行的路上不要急于求成.