对于Redis的学习,除了底层的原理,笔者认为,如何在各个场景中合理的设计使用Redis也是十分的关键的,Redis提供了五种数据类型,可以让我们在更多的场景中做出选择。接下来笔者就从实际案例出发,首先从Redis的分布式锁开始。在日常生活中,我们肯定有接触过抢票(抢商品)的场景,那么这个时候就会存在并发问题,下面的代码主要是在模拟一个抢票的场景。
RedisLockController
@RestController
public class RedisLockController {
private static long count = 20;
//CountDownLatch是一个并发工具类,具体的使用可以看我的并发编程章节,有详细讲解常用的并发工具类
private CountDownLatch countDownLatch = new CountDownLatch(5);
@Resource(name="redisLock")
private RedisLock lock;
@RequestMapping(value = "/sale", method = RequestMethod.GET)
public Long sale() throws InterruptedException {
count = 20;
countDownLatch = new CountDownLatch(5);
System.out.println("-------五个窗口开售20张票-------");
new PlusThread().start();
new PlusThread().start();
new PlusThread().start();
new PlusThread().start();
new PlusThread().start();
return count;
}
// 线程类模拟一个窗口买火车票
public class PlusThread extends Thread {
private int amount = 0;//当前线程成功抢了多少张票
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始售票");
countDownLatch.countDown();
if (countDownLatch.getCount()==0){
System.out.println("----------结果------------");
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
while (count > 0) {
//lock方法会自旋尝试获取锁,如果获取失败会递归调用自身lock,直到获取成功
lock.lock();
try {
if (count > 0) {
//模拟卖票业务处理
amount++;
count--;
}
}finally{
lock.unlock();
}
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "售出"+ (amount) + "张票");
}
}
}
RedisConfig
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(lettuceConnectionFactory);
// 这里可以给template加一些序列化的定制
return template;
}
}
RedisLock
@Component
public class RedisLock {
private static final String KEY = "LOCK_KEY";
@Resource(name = "redisTemplate")
private RedisTemplate<String, Object> redisTemplate;
//使用ThreadLocal这个类是在多线程中数据隔离的一种方式,具体底层原理,在高并发章节会详细讲解到
private static ThreadLocal<String> local = new ThreadLocal<>();
//加锁
public void lock() {
//1.尝试加锁,成功则直接返回表示当前线程获取锁成功
if(tryLock()){
return;
}
//2.加锁失败,当前任务休眠一段时间
try {
Thread.sleep(10);//这个睡眠时间根据业务需求而定
} catch (InterruptedException e) {
e.printStackTrace();
}
//3.睡眠结束,递归调用,再次去抢锁
lock();
}
//尝试获取锁,使用setNx命令返回OK的加锁成功,并生产随机值
public boolean tryLock() {
//产生随机值,标识本次锁编号
String uuid = UUID.randomUUID().toString();
RedisCallback<Boolean> callback = (connection) -> connection.set(KEY.getBytes(StandardCharsets.UTF_8), uuid.getBytes(StandardCharsets.UTF_8), Expiration.milliseconds(1000L), RedisStringCommands.SetOption.SET_IF_ABSENT);
boolean re = redisTemplate.execute(callback);
//设值成功--抢到了锁
if(re){
local.set(uuid);//抢锁成功,把锁标识号记录入本线程--- Threadlocal
return true;
}
//key值里面有了,我的uuid未能设入进去,抢锁失败
return false;
}
//解锁
public boolean unlock() {
//FileUtils工具类读取lua脚本
String script = FileUtils.getScript("unlock.lua");
//执行lua脚本
RedisCallback<Boolean> callback = (connection) -> connection.eval(script.getBytes(), ReturnType.BOOLEAN ,1, KEY.getBytes(StandardCharsets.UTF_8), local.get().getBytes(StandardCharsets.UTF_8));
return redisTemplate.execute(callback);
}
}
FileUtils
@Component
public class FileUtils {
public static String getScript(String fileName){
String path = FileUtils.class.getClassLoader().getResource(fileName).getPath();
return readFileByLines(path);
}
public static String readFileByLines(String fileName) {
FileInputStream file = null;
BufferedReader reader = null;
InputStreamReader inputFileReader = null;
String content = "";
String tempString = null;
try {
file = new FileInputStream(fileName);
inputFileReader = new InputStreamReader(file, "utf-8");
reader = new BufferedReader(inputFileReader);
// 一次读入一行,直到读入null为文件结束
while ((tempString = reader.readLine()) != null) {
content += tempString;
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e1) {
}
}
}
return content;
}
}
unlock.lua
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
运行结果:
那么如果我们在这个抢票系统中,不使用锁,会发生怎样的事情呢?我们将锁相关的代码注释掉,运行看看结果:可以发现,二十张票却出售了23张票