在传统的关系型数据当中,使用事务是我们最常见的操作。来到Redis当中这里有事务吗,Redis是支持事务的。但是这个事务跟关系型数据库的传统事务不一样,在关系型数据库当中我们可以对出现错误的sql进行回滚,但是在redis是没有这一说的。
在Redis事务当中,所有操作都是在提交的统一执行的,所以并没有回归操作,其实这个事务更像是批处理的感觉。以下就是事务常用的命令:
1、watch
2、unwatch
3、multi
4、exec
5、discard
以下就是使用Jedis使用事务的代码:
public static void main(String args[]){ GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxIdle(8); config.setMaxTotal(10); config.setMinIdle(2); config.setMaxWaitMillis(3000); jedisPool = new JedisPool(config, "localhost"); Jedis conn = jedisPool.getResource(); Transaction transaction = conn.multi(); Response<Long> newListPushResult = transaction.rpush("newList","A","B","C"); Response<List<String>> newListResponse = transaction.lrange("newList",0,-1); transaction.exec(); System.out.println("newListPushResult : " + newListPushResult.get()); for (String item : newListResponse.get()) { System.err.println( item + " "); } conn.close(); }
通过代码我们可以看到我们的执行结果是在exec之后才统一返回,所以Jedis会用一个Response对象最为事务对象transaction的执行放回值。如果我们在transaction执行exec方法之前调用response对象的get方法会出现异常:
Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.
事实上我们还可以使用exec方法的返回值获得事务执行过程的结果,但是这个显然是不够方便的:
public static void main(String args[]){ GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxIdle(8); config.setMaxTotal(10); config.setMinIdle(2); config.setMaxWaitMillis(3000); jedisPool = new JedisPool(config, "localhost"); Jedis conn = jedisPool.getResource(); Transaction transaction = conn.multi(); transaction.rpush("newList","A","B","C"); transaction.lrange("newList",0,-1); List<Object> result = transaction.exec(); System.out.println("newListPushResult : " + result.get(0)); List<String> newListValues = (List<String>) result.get(1); for (String item : newListValues) { System.err.println( item + " "); } conn.close(); }
事务其中最为只要的功能是实现锁,redis提供一个watch命令。watch是一种乐观锁,watch命令的参数为key,当我们watch了一个key的时候,在事务执行之前被修改了,事务是不会执行成功的。只要我们在watch到事务执这段时间未被修改事务才会执行成功。当然如果我们在业务的中途不需要监控这个key的变化了也可以使用unwatch命令进行取消watch。以下是watch在jedis事务代码上的使用(下面会模拟一段购买商品的业务):
public static void main(String args[]){ GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxIdle(8); config.setMaxTotal(10); config.setMinIdle(2); config.setMaxWaitMillis(3000); jedisPool = new JedisPool(config, "localhost"); Jedis conn = jedisPool.getResource(); //market 有序集合 score 商品价格 member 商品名称 conn.zadd("market",100,"ProductA"); conn.zadd("market",33,"ProductB"); //用户对象 conn.hset("user:1","funds","900"); //监控market和user:1两个KEY的变化 conn.watch("user:1","market"); Double price = conn.zscore("market","ProductA"); String funds_str = conn.hget("user:1","funds"); if(price == null || funds_str == null){ System.out.println("商品或用户信息不存在"); conn.unwatch(); return; } double funds = Double.valueOf(funds_str); if(price > funds){ System.out.println("余额不足"); conn.unwatch(); return ; } //模拟中图余额被修改了,导致不够钱买单 conn.hset("user:1","funds","20"); Transaction transaction = conn.multi(); transaction.hincrByFloat("user:1","funds",-price); transaction.zrem("market","ProductA"); transaction.zadd("user_package:1",price,"ProductA"); List<Object> result = transaction.exec(); if(result == null || result.size() == 0 ){ System.out.println(">>>执行失败,中途被watch的key出现修改"); }else{ System.out.println(">>>>执行成功"); } conn.close(); }
输出如下:
>>>执行失败,中途被watch的key出现修改。
我们可以使用redis-cli查看是否有做修改:
127.0.0.1:6379> ZRANGE market 0 -1
1) "ProductB"
2) "ProductA"
可以清楚看到是没有再market中删除 ProductA的,如果在测试过程中都可以使用 MONITOR监控命令,但是在正式环境需要慎重使用 MONITOR命令,因为这样会导致输出缓存暴增问题。虽然watch能解决我们对锁的需求,但是watch也不是一个完美的解决方案。因为watch是属于乐观锁,如果对于大并发的业务系统中显然是不合适的,因为redis的处理速度相当的快,所以导致很多时候会出现执行失败,每次失败我们都会在一段时间中尝试重试。这样会导致大量的带宽和资源损耗,重复执行也导致很多时候执行效率低下的问题。而且我们发现watch是针对key进行乐观锁的,其实在market这个集合当中,我只需要market中的ProductA不被修改和User的funds不被修改即可,并不需要将整个key进行监控。
为了弥补这个问题我们会使用setnx命令进行锁操作,其实setnx并不是什么锁,以下是redis中文社区对setnx命令的解释:
SETNX key value
将 key
的值设为 value
,当且仅当 key
不存在。
若给定的 key
已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
-
可用版本:
- >= 1.0.0 时间复杂度:
- O(1) 返回值:
-
设置成功,返回
1
。设置失败,返回0
。
public static void main(String args[]) throws InterruptedException { GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxIdle(8); config.setMaxTotal(10); config.setMinIdle(2); config.setMaxWaitMillis(3000); jedisPool = new JedisPool(config, "localhost"); Jedis conn = jedisPool.getResource(); //market 有序集合 score 商品价格 member 商品名称 conn.zadd("market", 100, "ProductA"); conn.zadd("market", 33, "ProductB"); //用户对象 conn.hset("user:1", "funds", "900"); long startTime = System.currentTimeMillis(); boolean isMarketProductLocked = false; boolean isUserFundsLocked = false; String locked_id = UUID.randomUUID().toString(); while (System.currentTimeMillis() - startTime < 3000) { if (!isMarketProductLocked) { if (conn.setnx("lock:market:productA", locked_id) == 1) { isMarketProductLocked = true; } } if (!isUserFundsLocked) { if (conn.setnx("lock:user:1:funds", locked_id) == 1) { isUserFundsLocked = true; } } if(isUserFundsLocked && isMarketProductLocked){ break; } Thread.sleep(80); } if (!(isMarketProductLocked && isUserFundsLocked)) { System.out.println("请求锁失败!"); return; } Double price = conn.zscore("market", "ProductA"); String funds_str = conn.hget("user:1", "funds"); if (price == null || funds_str == null) { System.out.println("商品或用户信息不存在"); conn.unwatch(); return; } double funds = Double.valueOf(funds_str); if (price > funds) { System.out.println("余额不足"); conn.unwatch(); return; } Transaction transaction = conn.multi(); transaction.hincrByFloat("user:1", "funds", -price); transaction.zrem("market", "ProductA"); transaction.zadd("user_package:1", price, "ProductA"); List<Object> result = transaction.exec(); if (result == null || result.size() == 0) { System.out.println(">>>执行失败,中途被watch的key出现修改"); } else { System.out.println(">>>>执行成功"); } conn.del("lock:market:productA"); conn.del("lock:user:1:funds"); conn.close(); }其实这是利用setnx的特性在业务中实现的元素级别悲观锁。以下是我写的一个业务测试锁的性能(模拟一堆用户对商城上购买商品的测试):
public class App { volatile public static long successCounter = 0; volatile public static long failCounter = 0; public static long time = 0; volatile public static JedisPool jedisPool; public static void main(String[] args) { GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxIdle(8); config.setMaxTotal(12); config.setMinIdle(5); config.setMaxTotal(30); config.setMaxWaitMillis(3000); jedisPool = new JedisPool(config, "localhost"); Jedis conn = jedisPool.getResource(); conn.flushAll(); List<User> userList = init(jedisPool); time = System.currentTimeMillis(); for (User item : userList) { try { new Thread(new BuyItem(jedisPool.getResource(), item)).start(); } catch (Exception e) { e.printStackTrace(); } } } static List<User> init(JedisPool pool) { List<User> users = new ArrayList<User>(); users.add(new User(1, "A", 9999999999d)); users.add(new User(2, "B", 9999999999d)); users.add(new User(3, "C", 9999999999d)); users.add(new User(4, "D", 9999999999d)); users.add(new User(5, "E", 9999999999d)); users.add(new User(6, "F", 9999999999d)); users.add(new User(7, "G", 9999999999d)); users.add(new User(8, "H", 9999999999d)); users.add(new User(9, "I", 9999999999d)); users.add(new User(10, "J", 9999999999d)); List<Grocery> groceries = new LinkedList<Grocery>(); for (int i = 0; i < 1000000; i++) { double price = new Random().nextInt(10) + 10; groceries.add(new Grocery("grocery_" + i, price)); } initUser(pool.getResource(), users); initMarket(pool.getResource(), groceries); return users; } static void initUser(Jedis conn, List<User> users) { Pipeline pipeline = conn.pipelined(); pipeline.multi(); for (User item : users) { String hashKey = "User:" + item.getId(); pipeline.hset(hashKey, "username", item.getUsername()); pipeline.hset(hashKey, "funds", String.valueOf(item.getFunds())); } pipeline.exec(); conn.close(); } static void initMarket(Jedis conn, List<Grocery> groceries) { Pipeline pipeline = conn.pipelined(); pipeline.multi(); int index = 0; for (Grocery item : groceries) { String zSetKey = "market"; pipeline.zadd(zSetKey, item.getPrice(), item.getName()); if (index++ >= 1000) { index = 0; pipeline.exec(); pipeline.multi(); } } pipeline.exec(); conn.close(); } static class BuyItem implements Runnable { private Jedis conn; private User user; public BuyItem(Jedis conn, User user) { this.conn = conn; this.user = user; } public void run() { String buyerKey = "User:" + user.getId(); String markterKey = "market"; String inventoryOfbuyerKey = "inventory:" + user.getId(); String salerKey = null; try { for (int i = 0; i < 5000; i++) { try { String id = UUID.randomUUID().toString(); long randmon = (long) (Math.random() * conn.zcard(markterKey)); String groceryKey = (String) conn.zrange(markterKey, randmon, randmon).toArray()[0]; boolean hasLock = false; String lockName = "lock:" + markterKey + ":" + groceryKey; long startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime <= 5000) { if (conn.setnx(lockName, id) == 1) { hasLock = true; break; } } if (!hasLock) { System.err.println("can not got a lock!"); failCounter++; continue; } double price = conn.zscore(markterKey, groceryKey); if (Double.valueOf(conn.hget(buyerKey, "funds")) < price) { return; } Transaction transaction = conn.multi(); transaction.hincrByFloat(buyerKey, "funds", -price); transaction.zadd(inventoryOfbuyerKey, price, groceryKey); transaction.zrem(markterKey, groceryKey); List<Object> result = transaction.exec(); transaction.close(); if (result == null || result.size() <= 0) { System.out.println("can not exec ! "); failCounter++; } else { successCounter++; } while (true) { conn.watch(lockName); if (id.equals(conn.get(lockName))) { conn.del(lockName); conn.unwatch(); break; } else { conn.unwatch(); break; } } } catch (Exception e) { e.printStackTrace(); App.failCounter++; } } } finally { System.out.println(">>>>>failCounter:" + App.failCounter); System.out.println(">>>>>successCounter:" + App.successCounter); System.out.println(">>>>>time:" + (System.currentTimeMillis() - time)); conn.close(); } } } }
输出如下:
>>>>>failCounter:0 失败为0
>>>>>successCounter:49820 成功交易49820次
>>>>>time:11948 耗时为11948毫秒
其实如果稍微做优化这个速度可以更快,不过我们这里是提现了setnx锁的作用而已。
Pipeline
其实Pipeline跟事务一样都是做批量操作的,如果不必要使用事务可以使用Pipeline,Pipeline比事务的速度更快,因为事务处理批量提交还需要其他的一些业务逻辑的。使用pipeline。
Pipeline pipeline = conn.pipelined(); Response<Long> rpushResult = pipeline.rpush("testList", "A", "B", "C"); Response<List<String>> testListResponse = pipeline.lrange("testList", 0, -1); pipeline.sync(); System.out.println("rpushResult:" + rpushResult.get()); for (String item : testListResponse.get()) { System.out.print( item + " "); }
当然可以使用pipeline.syncAndReturnAll(),这个跟transaction.exec的返回方式一样。
但是需要注意不要一次性搞一堆数据发出去,这样很可能会阻塞redis,因为人redis是单线程的去执行命令的,所以如果一个命令太慢可能会阻塞其他客户端所以使用flushall,keys * 这些操作也可能会阻塞客户端。pipline和transaction也同理,如果exec和sync一次性提交的命令非常多就非常有可能会阻塞redis,同时jedis读写也是会超时的,如果一次性pipeline提交的数据太多也可能会导致读写超时异常,所以我上面一次性写入market的测试数据都是通过分批次去提交的:
static void initMarket(Jedis conn, List<Grocery> groceries) { Pipeline pipeline = conn.pipelined(); pipeline.multi(); int index = 0; for (Grocery item : groceries) { String zSetKey = "market"; pipeline.zadd(zSetKey, item.getPrice(), item.getName()); if (index++ >= 1000) { index = 0; pipeline.exec(); pipeline.multi(); } } pipeline.exec(); conn.close(); }