redis秒杀系统数据同步(保证不多卖)

来源:互联网 发布:四川大学软件工程学院 编辑:程序博客网 时间:2024/06/05 14:26

东西不多卖


秒杀系统需要保证东西不多卖,关键是在多个客户端对库存进行减操作时,必须加锁。Redis中的Watch刚好可以实现一点。首先我们需要获取当前库存,只有库存中的食物小于购物车的数目才能对库存进行减。在高并发的情况下会出现某时刻查询库存够的,但下一时刻另外一个线程下单了,对库存进行减操作,刚好小于上个线程的购物车数目。照理现在的状态是不能下单成功的,因为库存已经不够了,但上一线程仍然认为数量还够,对库存进行减操作,从而导致库存出现负数的情况。如何避免?

Redis 中的watch可以在事务前对数据进行监控,如果在事务执行前,该数据发生改变,则事务不执行。刚好能满足我们的要求。看了很多代码,对watch功能还不是很理解,因为网上很多写的帖子都没有明确指出多客户端(理解之后发现还是有写的),所以不明白的可以参见下面的例子,是用Java写的。以下代码可以保证库存不多卖。

在redis中设置一个键为mykey,值为100的变量

package com.test.redis;import java.util.List;import redis.clients.jedis.Jedis;import redis.clients.jedis.Transaction;public class RedisLocal {public static void main(String[] args) {RedisUtil.getJedis().set("mykey", "100");new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();new MyThread().start();}}class MyThread extends Thread {Jedis jedis = null;@Overridepublic void run() {// TODO Auto-generated method stubwhile (true) {System.out.println(Thread.currentThread().getName());jedis = RedisUtil.getJedis();try {int stock = Integer.parseInt(jedis.get("mykey"));if (stock > 0) {jedis.watch("mykey");Transaction transaction = jedis.multi();transaction.set("mykey", String.valueOf(stock - 1));List<Object> result = transaction.exec();if (result == null || result.isEmpty()) {System.out.println("Transaction error...");// 可能是watch-key被外部修改,或者是数据操作被驳回}} else {System.out.println("库存为0");break;}} catch (Exception e) {// TODO: handle exceptione.printStackTrace();RedisUtil.returnResource(jedis);} finally {RedisUtil.returnResource(jedis);}}}}
对于Redis事务来说行不通,因为在exec命令之前,所有的命令都被Redis缓存起来了,根本就拿不到balance的值。那类似这种需要基于已经存在的某个值的事务在Redis中如何实现呢?答案是Watch命令:

redis.watch('balance')balance = redis.get('balance')if (balance < amtToSubtract) {    redis.unwatch()} else {    redis.multi()    redis.decrby('balance', amtToSubtract)    redis.incrby('debt', amtToSubtract)    redis.exec()}
通俗点讲,watch命令就是标记一个键,如果标记了一个键,在提交事务前如果该键被别人修改过,那事务就会失败,这种情况通常可以在程序中重新再尝试一次。像上面的例子,首先标记了键balance,然后检查余额是否足够,不足就取消标记,并不做扣减;足够的话,就启动事务进行更新操作,如果在此期间键balance被其它人修改,那在提交事务(执行exec)时就会报错,程序中通常可以捕获这类错误再重新执行一次,直到成功。
Redis事务失败后不支持回滚 与数据库事务很重要的一个区别是Redis事务在执行过程中出错后不会回滚。在exec命令后,Redis Server开始一个个的执行被缓存的命令,如果其中某个命令执行出错了,那之前的命令并不会被回滚。

RedisUtil

import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig;public final class RedisUtil {// Redis服务器IPprivate static String ADDR = "192.168.1.8";// Redis的端口号private static int PORT = 6379;// 访问密码private static String AUTH = "cl";// 可用连接实例的最大数目,默认值为8;// 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。private static int MAX_ACTIVE = 1024;// 控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。private static int MAX_IDLE = 200;// 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;private static int MAX_WAIT = 10000;private static int TIMEOUT = 10000;// 在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;private static boolean TEST_ON_BORROW = true;private static JedisPool jedisPool = null;/** * 初始化Redis连接池 */static {try {JedisPoolConfig config = new JedisPoolConfig();config.setMaxIdle(MAX_IDLE);config.setTestOnBorrow(TEST_ON_BORROW);jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);} catch (Exception e) {e.printStackTrace();}}/** * 获取Jedis实例 *  * @return */public synchronized static Jedis getJedis() {try {if (jedisPool != null) {Jedis resource = jedisPool.getResource();return resource;} else {return null;}} catch (Exception e) {e.printStackTrace();return null;}}/** * 释放jedis资源 *  * @param jedis */public static void returnResource(final Jedis jedis) {if (jedis != null) {jedisPool.returnResource(jedis);}}}

0 0
原创粉丝点击