开发环境:
- JDK11
- Redis3.2
Redis事务机制:
与传统的关系型数据库类似,NoSQL也存在许多并发访问的情况,因此出现了如何保证数据一致性的问题,处理的方式有很多。
针对不同的业务层次有不同的解决方案:
- 视图层:前端来保证数据一致性,笔者对前端技术熟悉程度还不足以搞定,暂不讨论;
- 业务层:可以使用线程同步来保证数据一致性;
- 持久层:在持久层解决数据一致性问题是最优的选择,此时有悲观锁、乐观锁等解决方案;
对于Redis而言,可以通过事务与锁保证数据一致性,首先了解下Redis事务,与关系性数据库流程类似,代码如下:
/**
* Jedis实现事务
* @return
*/
@GetMapping("test6")
public Object test6()
{
String key = "key_1";
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 开启事务
Transaction transaction = jedis.multi();
transaction.hset(key, "a", "新恒结衣");
transaction.hset(key, "b", "hello");
// 提交事务
List<Object> list = transaction.exec();
return list;
}
/**
* redisTemplate包装类实现事务
* @return
*/
@GetMapping("test7")
public Object test7()
{
SessionCallback<String> callback = new SessionCallback<>()
{
@Override
public String execute(RedisOperations operations) throws DataAccessException
{
operations.multi();
operations.opsForValue().set("key_1", "hello");
System.out.println(1 / 0);
operations.opsForValue().set("key_2", "world");
operations.exec();
return "ok";
}
};
Object result = null;
try
{
result = redisTemplate.execute(callback);
} catch (Exception e)
{
System.out.println("发生异常");
}
System.out.println("value1=" + redisTemplate.opsForValue().get("key_1"));
System.out.println("value2=" + redisTemplate.opsForValue().get("key_2"));
return result == null ? "result为空" : result;
}
如上代码展示了Redis实现事务的两种方式,重点看test7方法,事务中出现1/0的异常,而最后执行结果为返回result为空,value1=hello,value2=null,可以发现即使发生异常,事务也会提交,但异常后的数据定义语句不会执行且事务执行结果返回null。
Redis锁机制:
与传统关系型数据库类似,事务操作并不能保证数据的一致性,原因也特别简单,并发量比较大的时候同一个Redis服务器可能在执行多个事务,以抢购商品为例,假设库存var=100,对于客户端来说,每次取出var都需要一次判断,看var是否大于0,如果是则执行购买业务,反之则返回抢购完毕,以var=1为例,此时3个客户端几乎同时读取var,则均返回1,那么这三个客户端都会执行购买业务,造成var=-2的超发情况,程序示例如下:
public static void main(String[] args)
{
var context = new AnnotationConfigApplicationContext(RedisConfig.class);
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 初始库存100
jedis.set("var", "100");
test1();
try
{
// 等待测试方法执行完毕
Thread.sleep(5000);
}catch (Exception e)
{
e.printStackTrace();
}
System.out.println("test方法执行完毕,当前var="+jedis.get("var"));
}
/**
* 购买测试
*/
private static void test1()
{
// 模拟200次购买请求
for (int i = 0; i < 200; i++)
{
new Thread(() ->
{
Jedis jedis = new Jedis("127.0.0.1", 6379);
if (Integer.parseInt(jedis.get("var")) <= 0)
{
System.out.println("var已经不足");
}else
{
Transaction transaction = jedis.multi();
transaction.incrBy("var", -1);
System.out.println("成功购买");
transaction.exec();
}
}).start();
}
}
最后程序打印出var=-20(具体打印结果具有不确定性)。
接下来使用Redis的锁机制解决超发问题,程序如下:
/**
* 应用锁机制的购买测试
*/
private static void test2()
{
// 模拟200次购买请求
for (int i = 0; i < 200; i++)
{
new Thread(() ->
{
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 监控key
jedis.watch("var");
if (Integer.parseInt(jedis.get("var")) <= 0)
{
System.out.println("var已经不足");
return;
} else
{
Transaction transaction = jedis.multi();
transaction.incrBy("var", -1);
List list = transaction.exec();
if (list.size() == 0)
{
System.out.println("事务被取消,重新购买");
again(jedis);
} else
{
System.out.println("成功购买,list=" + list);
}
}
}).start();
}
}
/**
* 重新购买方法
* @param jedis
*/
private static void again(Jedis jedis)
{
jedis.watch("var");
if (Integer.parseInt(jedis.get("var")) <= 0)
{
System.out.println("var已经不足");
return;
}
Transaction transaction = jedis.multi();
transaction.incrBy("var", -1);
List list = transaction.exec();
if (list.size() == 0)
{
System.out.println("事务被取消,重新购买");
again(jedis);
} else
{
System.out.println("成功购买,list=" + list);
}
}
可能有些小伙伴会问为什么需要再写一个重新购买方法,原因是固定的循环200次中会有大量的购买失败(watch监控值改变引起的),如果不加重新购买方法会导致最后var值大于0,也就是明明已经200人抢购完毕,但还是有商品没卖出去。
Redis的锁机制是由watch命令控制的,它在事务开启之前去监控一个或者多个key,在所有事务命令入队执行前一刻会去查看该key是否被其他客户端修改过(注意是其他客户端,自身不算),如果没有则正常执行,如果有则事务回滚,返回list长度为0。
Redis锁机制应用流程:
- 定义Jedis对象;
- watch命令绑定监控的key;
- 开启事务,设置执行命令;
- 执行事务,并判断事务是否回滚;